Reserve enough space for ylabels
[pedometerwidget] / src / usr / lib / hildon-desktop / pedometer_widget_home.py
1 #Pedometer Home Widget
2 #Author: Mirestean Andrei < andrei.mirestean at gmail.com >
3 #
4 #This program is free software: you can redistribute it and/or modify
5 #it under the terms of the GNU General Public License as published by
6 #the Free Software Foundation, either version 3 of the License, or
7 #(at your option) any later version.
8 #
9 #This program is distributed in the hope that it will be useful,
10 #but WITHOUT ANY WARRANTY; without even the implied warranty of
11 #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 #GNU General Public License for more details.
13 #
14 #You should have received a copy of the GNU General Public License
15 #along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 import os
18 import time
19 import pickle
20 from datetime import date, timedelta
21 from xml.dom.minidom import getDOMImplementation, parseString
22
23 import gobject
24 import gconf
25 import gtk
26 import cairo
27
28 import hildondesktop
29 import hildon
30
31 PATH = "/apps/pedometerhomewidget"
32 MODE = PATH + "/mode"
33 HEIGHT = PATH + "/height"
34 UNIT = PATH + "/unit"
35 ASPECT = PATH + "/aspect"
36 SECONDVIEW = PATH + "/secondview"
37 GRAPHVIEW = PATH + "/graphview"
38 LOGGING = PATH + "/logging"
39
40 ICONSPATH = "/opt/pedometerhomewidget/"
41
42 unit = 0
43
44 class Singleton(object):
45     _instance = None
46     def __new__(cls, *args, **kwargs):
47         if not cls._instance:
48             cls._instance = super(Singleton, cls).__new__(
49                                 cls, *args, **kwargs)
50         return cls._instance
51
52 class PedoIntervalCounter(Singleton):
53     MIN_THRESHOLD = 500
54     MIN_TIME_STEPS = 0.5
55     x = []
56     y = []
57     z = []
58     t = []
59
60     #TODO: check if last detected step is at the end of the interval
61
62     def set_vals(self, coords, tval):
63         self.x = coords[0]
64         self.y = coords[1]
65         self.z = coords[2]
66         self.t = tval
67
68     def set_mode(self, mode):
69         #runnig, higher threshold to prevent fake steps
70         self.mode = mode
71         if mode == 1:
72             self.MIN_THRESHOLD = 650
73             self.MIN_TIME_STEPS = 0.35
74         #walking
75         else:
76             self.MIN_THRESHOLD = 500
77             self.MIN_TIME_STEPS = 0.5
78
79     def calc_mean(self, vals):
80         sum = 0
81         for i in vals:
82             sum += i
83         if len(vals) > 0:
84             return sum / len(vals)
85         return 0
86
87     def calc_stdev(self, vals):
88         rez = 0
89         mean = self.calc_mean(vals)
90         for i in vals:
91             rez += pow(abs(mean - i), 2)
92         return math.sqrt(rez / len(vals))
93
94     def calc_threshold(self, vals):
95         vmax = max(vals)
96         vmin = min(vals)
97         mean = self.calc_mean(vals)
98         threshold = max (abs(mean - vmax), abs(mean - vmin))
99         return threshold
100
101     def count_steps(self, vals, t):
102         threshold = self.MIN_THRESHOLD
103         mean = self.calc_mean(vals)
104         cnt = 0
105         i = 0
106         while i < len(vals):
107             if abs(vals[i] - mean) > threshold:
108                 cnt += 1
109                 ntime = t[i] + self.MIN_TIME_STEPS
110                 while i < len(vals) and t[i] < ntime:
111                     i += 1
112             i += 1
113         return cnt
114
115     def get_best_values(self, x, y, z):
116         dev1 = self.calc_stdev(x)
117         dev2 = self.calc_stdev(y)
118         dev3 = self.calc_stdev(z)
119         dev_max = max(dev1, dev2, dev3)
120
121         if (abs(dev1 - dev_max) < 0.001):
122             logger.info("X chosen as best axis, stdev %f" % dev1)
123             return x
124         elif (abs(dev2 - dev_max) < 0.001):
125             logger.info("Y chosen as best axis, stdev %f" % dev2)
126             return y
127         else:
128             logger.info("Z chosen as best axis, stdev %f" % dev3)
129             return z
130
131     def number_steps(self):
132         vals = self.get_best_values(self.x, self.y, self.z)
133         return self.count_steps(vals, self.t)
134
135 class PedoValues():
136     def __init__(self, time=0, steps=0, dist=0, calories=0):
137         self.time = time
138         self.steps = steps
139         self.calories = calories
140         self.dist = dist
141         self.unit = unit
142
143     def __add__(self, other):
144         return PedoValues(self.time + other.time,
145                           self.steps + other.steps,
146                           self.dist + other.dist,
147                           self.calories + other.calories)
148
149     def get_print_time(self):
150         tdelta = self.time
151         hours = int(tdelta / 3600)
152         tdelta -= 3600 * hours
153         mins = int(tdelta / 60)
154         tdelta -= 60 * mins
155         secs = int(tdelta)
156         strtime = "%.2d:%.2d:%.2d" % (hours, mins, secs)
157         return strtime
158
159     def get_print_distance(self):
160         if self.dist > 1000:
161             if self.unit == 0:
162                 return "%.2f km" % (self.dist / 1000)
163             else:
164                 return "%.2f mi" % (self.dist / 1609.344)
165         else:
166             if self.unit == 0:
167                 return "%d m" % self.dist
168             else:
169                 return "%d ft" % int(self.dist * 3.2808)
170
171     def get_avg_speed(self):
172         conv = 0
173         if self.unit:
174             conv = 2.23693629
175         else:
176             conv = 3.6
177
178         if self.time == 0:
179             return 0
180         speed = 1.0 * self.dist / self.time
181         return speed * conv
182
183     def get_print_avg_speed(self):
184         suffix = ""
185         conv = 0
186         if self.unit:
187             suffix = "mi/h"
188             conv = 2.23693629
189         else:
190             suffix = "km/h"
191             conv = 3.6
192
193         if self.time == 0:
194             return "N/A " + suffix
195         speed = 1.0 * self.dist / self.time
196         #convert from meters per second to km/h or mi/h
197         speed *= conv
198         return "%.2f %s" % (speed, suffix)
199
200     def get_print_steps(self):
201         return str(self.steps)
202
203     def get_print_calories(self):
204         return str(self.calories)
205
206 class PedoRepository(Singleton):
207     values = {}
208
209     def load(self):
210         raise NotImplementedError("Must be implemented by subclass")
211
212     def save(self):
213         raise NotImplementedError("Must be implemented by subclass")
214
215     def reset_values(self):
216         self.values = {}
217         self.save()
218
219     def get_history_count(self):
220         """return the number of days in the log"""
221         return len(values)
222
223     def get_values(self):
224         return self.values
225
226     def add_values(self, values, when=date.today()):
227         """add PedoValues values to repository """
228         try:
229             self.values[when] = self.values[when] + values
230         except KeyError:
231             self.values[when] = values
232
233     def get_last_7_days(self):
234         ret = []
235         day = date.today()
236         for i in range(7):
237             try:
238                 ret.append(self.values[day])
239             except KeyError:
240                 ret.append(PedoValues())
241             day = day - timedelta(days=1)
242         return ret
243
244     def get_last_weeks(self):
245         delta = timedelta(days=1)
246         day = date.today()
247         week = int(date.today().strftime("%W"))
248         val = PedoValues()
249         ret = []
250         for i in range(56):
251             try:
252                 val += self.values[day]
253             except KeyError:
254                 pass
255             w = int(day.strftime("%W"))
256             if w != week:
257                 ret.append(val)
258                 val = PedoValues()
259                 week = w
260                 if len(ret) == 7:
261                     break
262             day -= delta
263         return ret
264
265     def get_alltime_values(self):
266         ret = PedoValues()
267         for k, v in self.values.iteritems():
268             ret = ret + v
269         return ret
270
271     def get_today_values(self):
272         try:
273             return self.values[date.today()]
274         except KeyError:
275             return PedoValues()
276
277     def get_this_week_values(self):
278         day = date.today()
279         ret = PedoValues()
280         while True:
281             try:
282                 ret += self.values[day]
283             except:
284                 pass
285             if day.weekday() == 0:
286                 break
287             day = day - timedelta(days=1)
288
289         return ret
290
291 class PedoRepositoryXML(PedoRepository):
292     DIR = os.path.join(os.path.expanduser("~"), ".pedometer")
293     FILE = os.path.join(DIR, "data.xml")
294     FILE2 = os.path.join(DIR, "pickle.log")
295     def __init__(self):
296         if not os.path.exists(self.DIR):
297             os.makedirs(self.DIR)
298         PedoRepository.__init__(self)
299
300     def load(self):
301         try:
302             f = open(self.FILE, "r")
303             dom = parseString(f.read())
304             values = dom.getElementsByTagName("pedometer")[0]
305             for v in values.getElementsByTagName("date"):
306                 d = int(v.getAttribute("ordinal_day"))
307                 steps = int(v.getAttribute("steps"))
308                 calories = float(v.getAttribute("calories"))
309                 dist = float(v.getAttribute("dist"))
310                 time = float(v.getAttribute("time"))
311                 day = date.fromordinal(d)
312                 self.values[day] = PedoValues(time, steps, dist, calories)
313
314             f.close()
315         except Exception, e:
316             logger.error("Error while loading data from xml file: %s" % e)
317
318     def save(self):
319         try:
320             f = open(self.FILE, "w")
321
322             impl = getDOMImplementation()
323
324             newdoc = impl.createDocument(None, "pedometer", None)
325             top_element = newdoc.documentElement
326             for k, v in self.values.iteritems():
327                 d = newdoc.createElement('date')
328                 d.setAttribute("day", str(k.isoformat()))
329                 d.setAttribute("ordinal_day", str(k.toordinal()))
330                 d.setAttribute("steps", str(v.steps))
331                 d.setAttribute("time", str(v.time))
332                 d.setAttribute("dist", str(v.dist))
333                 d.setAttribute("calories", str(v.calories))
334                 top_element.appendChild(d)
335
336             newdoc.appendChild(top_element)
337             newdoc.writexml(f)
338             #f.write(newdoc.toprettyxml())
339             f.close()
340         except Exception, e:
341             logger.error("Error while saving data to xml file: %s" % e)
342
343
344 class PedoRepositoryPickle(PedoRepository):
345     DIR = os.path.join(os.path.expanduser("~"), ".pedometer")
346     FILE = os.path.join(DIR, "pickle.log")
347
348     def __init__(self):
349         if not os.path.exists(self.DIR):
350             os.makedirs(self.DIR)
351         PedoRepository.__init__(self)
352
353     def load(self):
354         try:
355             f = open(self.FILE, "rb")
356             self.values = pickle.load(f)
357             f.close()
358         except Exception, e:
359             logger.error("Error while loading pickle file: %s" % e)
360
361     def save(self):
362         try:
363             f = open(self.FILE, "wb")
364             pickle.dump(self.values, f)
365             f.close()
366         except Exception, e:
367             logger.error("Error while saving data to pickle: %s" % e)
368
369 class PedoController(Singleton):
370     mode = 0
371     unit = 0
372     height_interval = 0
373     #what to display in second view - 0 - alltime, 1 - today, 2 - week
374     second_view = 0
375     callback_update_ui = None
376
377     STEP_LENGTH = 0.7
378     #values for the two views in the widget ( current and day/week/alltime)
379     v = [PedoValues(), PedoValues()]
380
381     startTime = 0
382     is_running = False
383     graph_controller = None
384
385     def __init__(self):
386         self.pedometer = PedoCounter(self.steps_detected)
387         self.pedometerInterval = PedoIntervalCounter()
388         self.pedometerInterval.set_mode(self.mode)
389         self.repository = PedoRepositoryXML()
390         self.repository.load()
391
392         self.graph_controller = GraphController()
393         self.load_values()
394
395     def load_values(self):
396         if self.second_view == 0:
397             self.v[1] = self.repository.get_alltime_values()
398         elif self.second_view == 1:
399             self.v[1] = self.repository.get_today_values()
400         else:
401             self.v[1] = self.repository.get_this_week_values()
402
403     def save_values(self):
404         self.repository.add_values(self.v[0])
405         self.repository.save()
406         self.load_values()
407
408     def start_pedometer(self):
409         self.v[0] = PedoValues()
410         self.startTime = time.time()
411         self.is_running = True
412         self.pedometer.start()
413         self.notify_UI(True)
414
415     def stop_pedometer(self):
416         self.is_running = False
417         self.pedometer.request_stop()
418
419     def get_first(self):
420         return self.v[0]
421
422     def get_second(self):
423         if self.is_running:
424             return self.v[0] + self.v[1]
425         else:
426             return self.v[1]
427
428     def update_current(self):
429         """
430         Update distance and calories for current values based on new height, mode values
431         """
432         self.v[0].dist = self.get_distance(self.v[0].steps)
433         self.v[0].calories = self.get_calories(self.v[0].steps)
434
435     def steps_detected(self, cnt, last_steps=False):
436         self.v[0].steps += cnt
437         self.v[0].dist += self.get_distance(cnt)
438         self.v[0].calories += self.get_distance(cnt)
439         self.v[0].time = time.time() - self.startTime
440         if last_steps:
441             self.save_values()
442             self.notify_UI()
443         else:
444             self.notify_UI(True)
445
446     def set_mode(self, mode):
447         self.mode = mode
448         self.set_height(self.height_interval)
449         self.notify_UI()
450
451     def set_unit(self, new_unit):
452         self.unit = new_unit
453         unit = new_unit
454
455     def set_second_view(self, second_view):
456         self.second_view = second_view
457         self.load_values()
458         self.notify_UI()
459
460     def set_callback_ui(self, func):
461         self.callback_update_ui = func
462
463     def set_height(self, height_interval):
464         self.height_inteval = height_interval
465         #set height, will affect the distance
466         if height_interval == 0:
467             self.STEP_LENGTH = 0.59
468         elif height_interval == 1:
469             self.STEP_LENGTH = 0.64
470         elif height_interval == 2:
471             self.STEP_LENGTH = 0.71
472         elif height_interval == 3:
473             self.STEP_LENGTH = 0.77
474         elif height_interval == 4:
475             self.STEP_LENGTH = 0.83
476         #increase step length if RUNNING
477         if self.mode == 1:
478             self.STEP_LENGTH *= 1.45
479         self.notify_UI()
480
481     def get_distance(self, steps=None):
482         if steps == None:
483             steps = self.counter
484         return self.STEP_LENGTH * steps;
485
486     def get_calories(self, steps):
487         return steps
488
489     def notify_UI(self, optional=False):
490         if self.callback_update_ui is not None:
491             self.callback_update_ui()
492         self.graph_controller.update_ui(optional)
493
494 class PedoCounter(Singleton):
495     COORD_FNAME = "/sys/class/i2c-adapter/i2c-3/3-001d/coord"
496     COORD_FNAME_SDK = "/home/andrei/pedometer-widget-0.1/date.txt"
497     LOGFILE = "/home/user/log_pedometer"
498     #time in ms between two accelerometer data reads
499     COORD_GET_INTERVAL = 10
500
501     COUNT_INTERVAL = 5
502
503     interval_counter = None
504     stop_requested = False
505     update_function = None
506     logging = False
507     isRunning = False
508
509     def __init__(self, update_function=None):
510         if not os.path.exists(self.COORD_FNAME):
511             self.COORD_FNAME = self.COORD_FNAME_SDK
512
513         self.interval_counter = PedoIntervalCounter()
514         self.update_function = update_function
515
516     def set_logging(self, value):
517         self.logging = value
518
519     def get_rotation(self):
520         f = open(self.COORD_FNAME, 'r')
521         coords = [int(w) for w in f.readline().split()]
522         f.close()
523         return coords
524
525     def start(self):
526         logger.info("Counter started")
527         self.isRunning = True
528         self.stop_requested = False
529         if self.logging:
530             fname = "%d_%d_%d_%d_%d_%d" % time.localtime()[0:6]
531             self.file = open(self.LOGFILE + fname + ".txt", "w")
532         gobject.idle_add(self.run)
533
534     def run(self):
535         self.coords = [[], [], []]
536         self.stime = time.time()
537         self.t = []
538         gobject.timeout_add(self.COORD_GET_INTERVAL, self.read_coords)
539         return False
540
541     def read_coords(self):
542         x, y, z = self.get_rotation()
543         self.coords[0].append(int(x))
544         self.coords[1].append(int(y))
545         self.coords[2].append(int(z))
546         now = time.time() - self.stime
547         if self.logging:
548             self.file.write("%d %d %d %f\n" % (self.coords[0][-1], self.coords[1][-1], self.coords[2][-1], now))
549
550         self.t.append(now)
551         #call stop_interval
552         ret = True
553         if self.t[-1] > self.COUNT_INTERVAL or self.stop_requested:
554             ret = False
555             gobject.idle_add(self.stop_interval)
556         return ret
557
558     def stop_interval(self):
559         self.interval_counter.set_vals(self.coords, self.t)
560         cnt = self.interval_counter.number_steps()
561
562         logger.info("Number of steps detected for last interval %d, number of coords: %d" % (cnt, len(self.t)))
563
564         gobject.idle_add(self.update_function, cnt, self.stop_requested)
565
566         if self.stop_requested:
567             gobject.idle_add(self.stop)
568         else:
569             gobject.idle_add(self.run)
570         return False
571
572     def stop(self):
573         if self.logging:
574             self.file.close()
575         logger.info("Counter has finished")
576
577     def request_stop(self):
578         self.stop_requested = True
579         self.isRunning = False
580
581 class CustomButton(hildon.Button):
582     def __init__(self, icon):
583         hildon.Button.__init__(self, gtk.HILDON_SIZE_AUTO_WIDTH, hildon.BUTTON_ARRANGEMENT_VERTICAL)
584         self.icon = icon
585         self.set_size_request(int(32 * 1.4), int(30 * 1.0))
586         self.retval = self.connect("expose_event", self.expose)
587
588     def set_icon(self, icon):
589         self.icon = icon
590
591     def expose(self, widget, event):
592         self.context = widget.window.cairo_create()
593         self.context.rectangle(event.area.x, event.area.y,
594                             event.area.width, event.area.height)
595
596         self.context.clip()
597         rect = self.get_allocation()
598         self.context.rectangle(rect.x, rect.y, rect.width, rect.height)
599         self.context.set_source_rgba(1, 1, 1, 0)
600
601         style = self.rc_get_style()
602         color = style.lookup_color("DefaultBackgroundColor")
603         if self.state == gtk.STATE_ACTIVE:
604             style = self.rc_get_style()
605             color = style.lookup_color("SelectionColor")
606             self.context.set_source_rgba (color.red / 65535.0, color.green / 65335.0, color.blue / 65535.0, 0.75);
607         self.context.fill()
608
609         #img = cairo.ImageSurface.create_from_png(self.icon)
610
611         #self.context.set_source_surface(img)
612         #self.context.set_source_surface(img, rect.width/2 - img.get_width() /2, 0)
613         img = gtk.Image()
614         img.set_from_file(self.icon)
615         buf = img.get_pixbuf()
616         buf = buf.scale_simple(int(32 * 1.5), int(30 * 1.5), gtk.gdk.INTERP_BILINEAR)
617
618         self.context.set_source_pixbuf(buf, rect.x + (event.area.width / 2 - 15) - 8, rect.y + 1)
619         self.context.scale(200, 200)
620         self.context.paint()
621
622         return self.retval
623
624 class CustomEventBox(gtk.EventBox):
625
626     def __init__(self):
627         gtk.EventBox.__init__(self)
628
629     def do_expose_event(self, event):
630         self.context = self.window.cairo_create()
631         self.context.rectangle(event.area.x, event.area.y,
632                             event.area.width, event.area.height)
633
634         self.context.clip()
635         rect = self.get_allocation()
636         self.context.rectangle(rect.x, rect.y, rect.width, rect.height)
637
638         if self.state == gtk.STATE_ACTIVE:
639             style = self.rc_get_style()
640             color = style.lookup_color("SelectionColor")
641             self.context.set_source_rgba (color.red / 65535.0, color.green / 65335.0, color.blue / 65535.0, 0.75);
642         else:
643             self.context.set_source_rgba(1, 1, 1, 0)
644         self.context.fill()
645
646         gtk.EventBox.do_expose_event(self, event)
647
648 class GraphController(Singleton):
649     ytitles = ["Steps", "Average Speed", "Distance", "Calories"]
650     xtitles = ["Day", "Week"] # "Today"]
651     widget = None
652     def __init__(self):
653         self.repository = PedoRepositoryXML()
654         self.last_update = 0
655
656     def set_graph(self, widget):
657         self.widget = widget
658         self.update_ui()
659
660     def set_current_view(self, view):
661         """
662         current_view % len(ytitles) - gives the ytitle
663         current_view / len(ytitles) - gives the xtitle
664         """
665         self.current_view = view
666
667         if self.current_view == len(self.ytitles) * len(self.xtitles):
668             self.current_view = 0
669         self.x_id = self.current_view / len(self.ytitles)
670         self.y_id = self.current_view % len(self.ytitles)
671
672     def next_view(self):
673         self.set_current_view(self.current_view+1)
674         self.update_ui()
675         return self.current_view
676
677     def last_weeks_labels(self):
678         d = date.today()
679         delta = timedelta(days=7)
680         ret = []
681         for i in range(7):
682             ret.append(d.strftime("Week %W"))
683             d = d - delta
684         return ret
685
686     def compute_values(self):
687         labels = []
688         if self.x_id == 0:
689             values = self.repository.get_last_7_days()
690             d = date.today()
691             delta = timedelta(days=1)
692             for i in range(7):
693                 labels.append(d.ctime().split()[0])
694                 d = d - delta
695
696         elif self.x_id == 1:
697             values = self.repository.get_last_weeks()
698             d = date.today()
699             for i in range(7):
700                 labels.append(d.strftime("Week %W"))
701                 d = d - timedelta(days=7)
702         else:
703             values = self.repository.get_today()
704             #TODO get labels
705
706         if self.y_id == 0:
707             yvalues = [line.steps for line in values]
708         elif self.y_id == 1:
709             yvalues = [line.get_avg_speed() for line in values]
710         elif self.y_id == 2:
711             yvalues = [line.dist for line in values]
712         else:
713             yvalues = [line.calories for line in values]
714
715         #determine values for y lines in graph
716         diff = self.get_best_interval_value(max(yvalues))
717         ytext = []
718         for i in range(6):
719             ytext.append(str(int(i*diff)))
720
721         if self.widget is not None:
722             yvalues.reverse()
723             labels.reverse()
724             self.widget.values = yvalues
725             self.widget.ytext = ytext
726             self.widget.xtext = labels
727             self.widget.max_value = diff * 5
728             self.widget.text = self.xtitles[self.x_id] + " / " + self.ytitles[self.y_id]
729             self.widget.queue_draw()
730         else:
731             logger.error("Widget not set in GraphController")
732
733     def get_best_interval_value(self, max_value):
734         diff =  1.0 * max_value / 5
735         l = len(str(int(diff)))
736         d = math.pow(10, l/2)
737         val = int(math.ceil(1.0 * diff / d)) * d
738         if val == 0:
739             val = 1
740         return val
741
742     def update_ui(self, optional=False):
743         """update graph values every x seconds"""
744         if optional and self.last_update - time.time() < 600:
745             return
746         if self.widget is None:
747             return
748
749         self.compute_values()
750         self.last_update = time.time()
751
752 class GraphWidget(gtk.DrawingArea):
753
754     def __init__(self):
755         gtk.DrawingArea.__init__(self)
756         self.set_size_request(-1, 150)
757         self.yvalues = 5
758
759         """sample values"""
760         self.ytext = ["   0", "1000", "2000", "3000", "4000", "5000"]
761         self.xtext = ["Monday", "Tuesday", "Wednesday","Thursday", "Friday", "Saturday", "Sunday"]
762         self.values = [1500, 3400, 4000, 3600, 3200, 0, 4500]
763         self.max_value = 5000
764         self.text = "All time steps"
765
766     def do_expose_event(self, event):
767         context = self.window.cairo_create()
768
769         # set a clip region for the expose event
770         context.rectangle(event.area.x, event.area.y,
771                                event.area.width, event.area.height)
772         context.clip()
773
774         context.save()
775
776         context.set_operator(cairo.OPERATOR_SOURCE)
777         style = self.rc_get_style()
778
779         if self.state == gtk.STATE_ACTIVE:
780             color = style.lookup_color("SelectionColor")
781         else:
782              color = style.lookup_color("DefaultBackgroundColor")
783         context.set_source_rgba (color.red / 65535.0, color.green / 65335.0, color.blue / 65535.0, 0.75)
784
785         context.paint()
786         context.restore();
787         self.draw(context)
788
789     def draw(self, cr):
790         space_below = 20
791         space_above = 10
792         border_right = 10
793         border_left = 30
794
795         rect = self.get_allocation()
796         x = rect.width
797         y = rect.height
798
799         cr.select_font_face("Purisa", cairo.FONT_SLANT_NORMAL,
800             cairo.FONT_WEIGHT_NORMAL)
801         cr.set_font_size(13)
802
803         #check space needed to display ylabels
804         te = cr.text_extents(self.ytext[-1])
805         border_left = te[2] + 7
806
807         cr.set_source_rgb(1, 1, 1)
808         cr.move_to(border_left, space_above)
809         cr.line_to(border_left, y-space_below)
810         cr.set_line_width(2)
811         cr.stroke()
812
813         cr.move_to(border_left, y-space_below)
814         cr.line_to(x-border_right, y-space_below)
815         cr.set_line_width(2)
816         cr.stroke()
817
818         ydiff = (y-space_above-space_below) / self.yvalues
819         for i in range(self.yvalues):
820             yy = y-space_below-ydiff*(i+1)
821             cr.move_to(border_left, yy)
822             cr.line_to(x-border_right, yy)
823             cr.set_line_width(0.8)
824             cr.stroke()
825
826
827         for i in range(6):
828             yy = y - space_below - ydiff*i + 5
829             te = cr.text_extents(self.ytext[i])
830
831             cr.move_to(border_left-te[2]-2, yy)
832             cr.show_text(self.ytext[i])
833
834         cr.set_font_size(15)
835         te = cr.text_extents(self.text)
836         cr.move_to((x-te[2])/2, y-5)
837         cr.show_text(self.text)
838
839         graph_x_space = x - border_left - border_right
840         graph_y_space = y - space_below - space_above
841         bar_width = graph_x_space*0.75 / len(self.values)
842         bar_distance = graph_x_space*0.25 / (1+len(self.values))
843
844         #set dummy max value to avoid exceptions
845         if self.max_value == 0:
846             self.max_value = 100
847         for i in range(len(self.values)):
848             xx = border_left + (i+1)*bar_distance + i * bar_width
849             yy = y-space_below
850             height = graph_y_space * (1.0 * self.values[i] / self.max_value)
851             cr.set_source_rgba(1, 1, 1, 0.75)
852             cr.rectangle(int(xx), int(yy-height), int(bar_width), int(height))
853             cr.fill()
854
855         cr.set_source_rgba(1, 1, 1, 1)
856         cr.select_font_face("Purisa", cairo.FONT_SLANT_NORMAL,
857                             cairo.FONT_WEIGHT_NORMAL)
858         cr.set_font_size(13)
859
860         cr.rotate(2*math.pi * (-45) / 180)
861         for i in range(len(self.values)):
862             xx = y - space_below - 10
863             yy = border_left + (i+1)*bar_distance + i * bar_width
864             cr.move_to(-xx, yy + bar_width*1.25 / 2)
865             cr.show_text(self.xtext[i])
866
867 class PedometerHomePlugin(hildondesktop.HomePluginItem):
868     button = None
869
870     #labels to display
871     labels = ["timer", "count", "dist", "avgSpeed", "calories"]
872
873     #current view
874     labelsC = {}
875
876     #second view ( day / week/ alltime)
877     labelsT = {}
878
879     second_view_labels = ["All-time", "Today", "This week"]
880
881     controller = None
882     pedometer = None
883     pedometerInterval = None
884     graph_controller = None
885     startTime = None
886
887     mode = 0
888     height = 0
889     unit = 0
890     aspect = 0
891     second_view = 0
892     logging = False
893
894     def __init__(self):
895         hildondesktop.HomePluginItem.__init__(self)
896
897         gobject.type_register(CustomEventBox)
898         gobject.type_register(GraphWidget)
899
900         self.client = gconf.client_get_default()
901         try:
902             self.mode = self.client.get_int(MODE)
903             self.height = self.client.get_int(HEIGHT)
904             self.unit = self.client.get_int(UNIT)
905             self.aspect = self.client.get_int(ASPECT)
906             self.second_view = self.client.get_int(SECONDVIEW)
907             self.graph_view = self.client.get_int(GRAPHVIEW)
908             self.logging = self.client.get_bool(LOGGING)
909
910         except:
911             self.client.set_int(MODE, 0)
912             self.client.set_int(HEIGHT, 0)
913             self.client.set_int(UNIT, 0)
914             self.client.set_int(ASPECT, 0)
915             self.client.set_int(SECONDVIEW, 0)
916             self.client.set_int(GRAPHVIEW, 0)
917             self.client.set_bool(LOGGING, False)
918
919         self.controller = PedoController()
920         self.controller.set_height(self.height)
921         self.controller.set_mode(self.mode)
922         self.controller.set_unit(self.unit)
923         self.controller.set_second_view(self.second_view)
924         self.controller.set_callback_ui(self.update_values)
925
926         self.graph_controller = GraphController()
927         self.graph_controller.set_current_view(self.graph_view)
928
929         self.button = CustomButton(ICONSPATH + "play.png")
930         self.button.connect("clicked", self.button_clicked)
931
932         self.create_labels(self.labelsC)
933         self.create_labels(self.labelsT)
934         self.label_second_view = self.new_label_heading(self.second_view_labels[self.second_view])
935
936         self.update_current()
937         self.update_total()
938
939         mainHBox = gtk.HBox(spacing=1)
940
941         descVBox = gtk.VBox(spacing=1)
942         descVBox.add(self.new_label_heading())
943         descVBox.add(self.new_label_heading("Time:"))
944         descVBox.add(self.new_label_heading("Steps:"))
945         descVBox.add(self.new_label_heading("Calories:"))
946         descVBox.add(self.new_label_heading("Distance:"))
947         descVBox.add(self.new_label_heading("Avg Speed:"))
948
949         currentVBox = gtk.VBox(spacing=1)
950         currentVBox.add(self.new_label_heading("Current"))
951         currentVBox.add(self.labelsC["timer"])
952         currentVBox.add(self.labelsC["count"])
953         currentVBox.add(self.labelsC["calories"])
954         currentVBox.add(self.labelsC["dist"])
955         currentVBox.add(self.labelsC["avgSpeed"])
956         self.currentBox = currentVBox
957
958         totalVBox = gtk.VBox(spacing=1)
959         totalVBox.add(self.label_second_view)
960         totalVBox.add(self.labelsT["timer"])
961         totalVBox.add(self.labelsT["count"])
962         totalVBox.add(self.labelsT["calories"])
963         totalVBox.add(self.labelsT["dist"])
964         totalVBox.add(self.labelsT["avgSpeed"])
965         self.totalBox = totalVBox
966
967         buttonVBox = gtk.VBox(spacing=1)
968         buttonVBox.add(self.new_label_heading(""))
969         buttonVBox.add(self.button)
970         buttonVBox.add(self.new_label_heading(""))
971
972         eventBox = CustomEventBox()
973         eventBox.set_visible_window(False)
974         eventBox.add(totalVBox)
975         eventBox.connect("button-press-event", self.eventBox_clicked)
976         eventBox.connect("button-release-event", self.eventBox_clicked_release)
977
978
979         mainHBox.add(buttonVBox)
980         mainHBox.add(descVBox)
981         mainHBox.add(currentVBox)
982         mainHBox.add(eventBox)
983         self.mainhbox = mainHBox
984
985         graph = GraphWidget()
986         self.graph_controller.set_graph(graph)
987
988         eventBoxGraph = CustomEventBox()
989         eventBoxGraph.set_visible_window(False)
990         eventBoxGraph.add(graph)
991         self.graph = graph
992         eventBoxGraph.connect("button-press-event", self.eventBoxGraph_clicked)
993         eventBoxGraph.connect("button-release-event", self.eventBoxGraph_clicked_release)
994
995         self.mainvbox = gtk.VBox()
996
997         self.mainvbox.add(mainHBox)
998         self.mainvbox.add(eventBoxGraph)
999
1000         self.mainvbox.show_all()
1001         self.add(self.mainvbox)
1002         self.update_aspect()
1003
1004         self.connect("unrealize", self.close_requested)
1005         self.set_settings(True)
1006         self.connect("show-settings", self.show_settings)
1007
1008     def eventBoxGraph_clicked(self, widget, data=None):
1009         widget.set_state(gtk.STATE_ACTIVE)
1010
1011     def eventBoxGraph_clicked_release(self, widget, data=None):
1012         self.graph_view = self.graph_controller.next_view()
1013         self.client.set_int(GRAPHVIEW, self.graph_view)
1014
1015         widget.set_state(gtk.STATE_NORMAL)
1016
1017     def eventBox_clicked(self, widget, data=None):
1018         widget.set_state(gtk.STATE_ACTIVE)
1019
1020     def eventBox_clicked_release(self, widget, data=None):
1021         widget.set_state(gtk.STATE_NORMAL)
1022
1023         self.second_view = (self.second_view + 1) % 3
1024         self.controller.set_second_view(self.second_view)
1025         self.client.set_int(SECONDVIEW, self.second_view)
1026
1027     def new_label_heading(self, title=""):
1028         l = gtk.Label(title)
1029         hildon.hildon_helper_set_logical_font(l, "SmallSystemFont")
1030         return l
1031
1032     def create_labels(self, new_labels):
1033         for label in self.labels:
1034             l = gtk.Label()
1035             hildon.hildon_helper_set_logical_font(l, "SmallSystemFont")
1036             hildon.hildon_helper_set_logical_color(l, gtk.RC_FG, gtk.STATE_NORMAL, "ActiveTextColor")
1037             new_labels[label] = l
1038
1039     def update_aspect(self):
1040         if self.aspect == 0:
1041             self.currentBox.show_all()
1042             self.totalBox.show_all()
1043         elif self.aspect == 1:
1044             self.currentBox.show_all()
1045             self.totalBox.hide_all()
1046         else:
1047             self.currentBox.hide_all()
1048             self.totalBox.show_all()
1049
1050     def update_ui_values(self, labels, values):
1051         labels["timer"].set_label(values.get_print_time())
1052         labels["count"].set_label(values.get_print_steps())
1053         labels["dist"].set_label(values.get_print_distance())
1054         labels["avgSpeed"].set_label(values.get_print_avg_speed())
1055         labels["calories"].set_label(values.get_print_calories())
1056
1057     def update_current(self):
1058         self.update_ui_values(self.labelsC, self.controller.get_first())
1059
1060     def update_total(self):
1061         self.update_ui_values(self.labelsT, self.controller.get_second())
1062
1063     def show_settings(self, widget):
1064         def reset_total_counter(arg):
1065             widget.totalCounter = 0
1066             widget.totalTime = 0
1067             widget.update_total()
1068             hildon.hildon_banner_show_information(self, "None", "Total counter was resetted")
1069
1070         def selector_changed(selector, data):
1071             widget.mode = selector.get_active(0)
1072             widget.client.set_int(MODE, widget.mode)
1073             widget.controller.set_mode(widget.mode)
1074
1075         def selectorH_changed(selector, data):
1076             widget.height = selectorH.get_active(0)
1077             widget.client.set_int(HEIGHT, widget.height)
1078             widget.controller.set_height(widget.height)
1079
1080
1081         def selectorUnit_changed(selector, data):
1082             widget.unit = selectorUnit.get_active(0)
1083             widget.client.set_int(UNIT, widget.unit)
1084             widget.controller.set_unit(widget.unit)
1085
1086         def selectorUI_changed(selector, data):
1087             widget.aspect = selectorUI.get_active(0)
1088             widget.client.set_int(ASPECT, widget.aspect)
1089             widget.update_aspect()
1090
1091         def logButton_changed(checkButton):
1092             widget.logging = checkButton.get_active()
1093             widget.client.set_bool(LOGGING, widget.logging)
1094
1095         dialog = gtk.Dialog()
1096         dialog.set_title("Settings")
1097
1098         dialog.add_button("OK", gtk.RESPONSE_OK)
1099         button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1100         button.set_title("Reset total counter")
1101         button.set_alignment(0, 0.8, 1, 1)
1102         button.connect("clicked", reset_total_counter)
1103
1104         selector = hildon.TouchSelector(text=True)
1105         selector.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
1106         selector.append_text("Walk")
1107         selector.append_text("Run")
1108         selector.connect("changed", selector_changed)
1109
1110         modePicker = hildon.ButtonPickerButton(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1111         modePicker.set_alignment(0.0, 0.5, 1.0, 1.0)
1112         modePicker.set_title("Select mode")
1113         modePicker.set_selector(selector)
1114         modePicker.set_active(widget.mode)
1115
1116         selectorH = hildon.TouchSelector(text=True)
1117         selectorH.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
1118         selectorH.append_text("< 1.50 m")
1119         selectorH.append_text("1.50 - 1.65 m")
1120         selectorH.append_text("1.66 - 1.80 m")
1121         selectorH.append_text("1.81 - 1.95 m")
1122         selectorH.append_text(" > 1.95 m")
1123         selectorH.connect("changed", selectorH_changed)
1124
1125         heightPicker = hildon.PickerButton(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1126         heightPicker.set_alignment(0.0, 0.5, 1.0, 1.0)
1127         heightPicker.set_title("Select height")
1128         heightPicker.set_selector(selectorH)
1129         heightPicker.set_active(widget.height)
1130
1131         selectorUnit = hildon.TouchSelector(text=True)
1132         selectorUnit.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
1133         selectorUnit.append_text("Metric (km)")
1134         selectorUnit.append_text("English (mi)")
1135         selectorUnit.connect("changed", selectorUnit_changed)
1136
1137         unitPicker = hildon.PickerButton(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1138         unitPicker.set_alignment(0.0, 0.5, 1.0, 1.0)
1139         unitPicker.set_title("Units")
1140         unitPicker.set_selector(selectorUnit)
1141         unitPicker.set_active(widget.unit)
1142
1143         selectorUI = hildon.TouchSelector(text=True)
1144         selectorUI = hildon.TouchSelector(text=True)
1145         selectorUI.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
1146         selectorUI.append_text("Show current + total")
1147         selectorUI.append_text("Show only current")
1148         selectorUI.append_text("Show only total")
1149         selectorUI.connect("changed", selectorUI_changed)
1150
1151         UIPicker = hildon.PickerButton(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
1152         UIPicker.set_alignment(0.0, 0.5, 1.0, 1.0)
1153         UIPicker.set_title("Widget aspect")
1154         UIPicker.set_selector(selectorUI)
1155         UIPicker.set_active(widget.aspect)
1156
1157         logButton = hildon.CheckButton(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT)
1158         logButton.set_label("Log data")
1159         logButton.set_active(widget.logging)
1160         logButton.connect("toggled", logButton_changed)
1161
1162         pan_area = hildon.PannableArea()
1163         vbox = gtk.VBox()
1164         vbox.add(button)
1165         vbox.add(modePicker)
1166         vbox.add(heightPicker)
1167         vbox.add(unitPicker)
1168         vbox.add(UIPicker)
1169         #vbox.add(logButton)
1170
1171         pan_area.add_with_viewport(vbox)
1172         pan_area.set_size_request(-1, 300)
1173
1174         dialog.vbox.add(pan_area)
1175         dialog.show_all()
1176         response = dialog.run()
1177         #hildon.hildon_banner_show_information(self, "None", "You have to Stop/Start the counter to apply the new settings")
1178         dialog.destroy()
1179
1180     def close_requested(self, widget):
1181         if self.pedometer is None:
1182             return
1183
1184         self.pedometer.request_stop()
1185
1186     def update_values(self):
1187         #TODO: do not update if the widget is not on the active desktop
1188         self.label_second_view.set_label(self.second_view_labels[self.second_view])
1189         self.update_current()
1190         self.update_total()
1191
1192     def button_clicked(self, button):
1193         if self.controller.is_running:
1194             self.controller.stop_pedometer()
1195             self.button.set_icon(ICONSPATH + "play.png")
1196         else:
1197             self.controller.start_pedometer()
1198             self.button.set_icon(ICONSPATH + "stop.png")
1199
1200     def do_expose_event(self, event):
1201         cr = self.window.cairo_create()
1202         cr.region(event.window.get_clip_region())
1203         cr.clip()
1204         #cr.set_source_rgba(0.4, 0.64, 0.564, 0.5)
1205         style = self.rc_get_style()
1206         color = style.lookup_color("DefaultBackgroundColor")
1207         cr.set_source_rgba (color.red / 65535.0, color.green / 65335.0, color.blue / 65535.0, 0.75);
1208
1209         radius = 5
1210         width = self.allocation.width
1211         height = self.allocation.height
1212
1213         x = self.allocation.x
1214         y = self.allocation.y
1215
1216         cr.move_to(x + radius, y)
1217         cr.line_to(x + width - radius, y)
1218         cr.curve_to(x + width - radius, y, x + width, y, x + width, y + radius)
1219         cr.line_to(x + width, y + height - radius)
1220         cr.curve_to(x + width, y + height - radius, x + width, y + height, x + width - radius, y + height)
1221         cr.line_to(x + radius, y + height)
1222         cr.curve_to(x + radius, y + height, x, y + height, x, y + height - radius)
1223         cr.line_to(x, y + radius)
1224         cr.curve_to(x, y + radius, x, y, x + radius, y)
1225
1226         cr.set_operator(cairo.OPERATOR_SOURCE)
1227         cr.fill_preserve()
1228
1229         color = style.lookup_color("ActiveTextColor")
1230         cr.set_source_rgba (color.red / 65535.0, color.green / 65335.0, color.blue / 65535.0, 0.5);
1231         cr.set_line_width(1)
1232         cr.stroke()
1233
1234         hildondesktop.HomePluginItem.do_expose_event(self, event)
1235
1236     def do_realize(self):
1237         screen = self.get_screen()
1238         self.set_colormap(screen.get_rgba_colormap())
1239         self.set_app_paintable(True)
1240         hildondesktop.HomePluginItem.do_realize(self)
1241
1242 hd_plugin_type = PedometerHomePlugin
1243
1244 # The code below is just for testing purposes.
1245 # It allows to run the widget as a standalone process.
1246 if __name__ == "__main__":
1247     import gobject
1248     gobject.type_register(hd_plugin_type)
1249     obj = gobject.new(hd_plugin_type, plugin_id="plugin_id")
1250     obj.show_all()
1251     gtk.main()
1252
1253 ############### old pedometer.py ###
1254 import math
1255 import logging
1256
1257 logger = logging.getLogger("pedometer")
1258 logger.setLevel(logging.INFO)
1259
1260 ch = logging.StreamHandler()
1261 formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
1262 ch.setFormatter(formatter)
1263 logger.addHandler(ch)
1264