1 #ifdef HAVE_CONFIG_H
2 #include <config.h>
3 #endif
4
5 #include <cairo.h>
6 #include <sys/time.h>
7 #include <gtk/gtk.h>
8 #include <math.h>
9
10 #include "clock.h"
11 #include "clock-map.h"
12 #include "clock-sunpos.h"
13 #include "clock-marshallers.h"
14
15 enum {
16 NEED_LOCATIONS,
17 LAST_SIGNAL
18 };
19
20 enum {
21 MARKER_NORMAL = 0,
22 MARKER_HILIGHT,
23 MARKER_CURRENT,
24 MARKER_NB
25 };
26
27 static char *marker_files[MARKER_NB] = {
28 "clock-map-location-marker.png",
29 "clock-map-location-hilight.png",
30 "clock-map-location-current.png"
31 };
32
33 static guint signals[LAST_SIGNAL] = { 0 };
34
35 typedef struct {
36 time_t last_refresh;
37
38 gint width;
39 gint height;
40
41 guint highlight_timeout_id;
42
43 GdkPixbuf *stock_map_pixbuf;
44 GdkPixbuf *location_marker_pixbuf[MARKER_NB];
45
46 GdkPixbuf *location_map_pixbuf;
47
48 /* The shadow itself */
49 GdkPixbuf *shadow_pixbuf;
50
51 /* The map with the shadow composited onto it */
52 GdkPixbuf *shadow_map_pixbuf;
53 } ClockMapPrivate;
54
55 G_DEFINE_TYPE_WITH_PRIVATE (ClockMap, clock_map, GTK_TYPE_WIDGET)
56
57 static void clock_map_finalize (GObject *);
58 static void clock_map_size_allocate (GtkWidget *this,
59 GtkAllocation *allocation);
60 static gboolean clock_map_draw (GtkWidget *this,
61 cairo_t *cr);
62 static void clock_map_get_preferred_width (GtkWidget *widget,
63 gint *minimum_width,
64 gint *natural_width);
65 static void clock_map_get_preferred_height (GtkWidget *widget,
66 gint *minimum_height,
67 gint *natural_height);
68 static void clock_map_place_locations (ClockMap *this);
69 static void clock_map_render_shadow (ClockMap *this);
70 static void clock_map_display (ClockMap *this);
71
72 ClockMap *
clock_map_new(void)73 clock_map_new (void)
74 {
75 ClockMap *this;
76
77 this = g_object_new (CLOCK_MAP_TYPE, NULL);
78
79 return this;
80 }
81
82 static void
clock_map_class_init(ClockMapClass * this_class)83 clock_map_class_init (ClockMapClass *this_class)
84 {
85 GObjectClass *g_obj_class = G_OBJECT_CLASS (this_class);
86 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (g_obj_class);
87
88 g_obj_class->finalize = clock_map_finalize;
89
90 /* GtkWidget signals */
91
92 widget_class->size_allocate = clock_map_size_allocate;
93 widget_class->draw = clock_map_draw;
94 widget_class->get_preferred_width = clock_map_get_preferred_width;
95 widget_class->get_preferred_height = clock_map_get_preferred_height;
96
97 /**
98 * ClockMap::need-locations
99 *
100 * The map widget emits this signal when it needs to know which
101 * locations to display.
102 *
103 * Returns: the handler should return a (GSList *) of (ClockLocation *).
104 * The map widget will not modify this list, so the caller should keep
105 * it alive.
106 */
107 signals[NEED_LOCATIONS] = g_signal_new ("need-locations",
108 G_TYPE_FROM_CLASS (g_obj_class),
109 G_SIGNAL_RUN_LAST,
110 G_STRUCT_OFFSET (ClockMapClass, need_locations),
111 NULL,
112 NULL,
113 _clock_marshal_POINTER__VOID,
114 G_TYPE_POINTER, 0);
115 }
116
117 static void
clock_map_init(ClockMap * this)118 clock_map_init (ClockMap *this)
119 {
120 int i;
121 ClockMapPrivate *priv = clock_map_get_instance_private (this);
122
123 gtk_widget_set_has_window (GTK_WIDGET (this), FALSE);
124
125 priv->last_refresh = 0;
126 priv->width = 0;
127 priv->height = 0;
128 priv->highlight_timeout_id = 0;
129 priv->stock_map_pixbuf = NULL;
130
131 g_assert (sizeof (marker_files)/sizeof (char *) == MARKER_NB);
132
133 for (i = 0; i < MARKER_NB; i++) {
134 char *resource;
135
136 resource = g_strconcat (CLOCK_RESOURCE_PATH "icons/", marker_files[i], NULL);
137 priv->location_marker_pixbuf[i] = gdk_pixbuf_new_from_resource (resource, NULL);
138 g_free (resource);
139 }
140 }
141
142 static void
clock_map_finalize(GObject * g_obj)143 clock_map_finalize (GObject *g_obj)
144 {
145 ClockMapPrivate *priv = clock_map_get_instance_private (CLOCK_MAP(g_obj));
146 int i;
147
148 if (priv->highlight_timeout_id) {
149 g_source_remove (priv->highlight_timeout_id);
150 priv->highlight_timeout_id = 0;
151 }
152
153 if (priv->stock_map_pixbuf) {
154 g_object_unref (priv->stock_map_pixbuf);
155 priv->stock_map_pixbuf = NULL;
156 }
157
158 for (i = 0; i < MARKER_NB; i++) {
159 if (priv->location_marker_pixbuf[i]) {
160 g_object_unref (priv->location_marker_pixbuf[i]);
161 priv->location_marker_pixbuf[i] = NULL;
162 }
163 }
164
165 if (priv->location_map_pixbuf) {
166 g_object_unref (priv->location_map_pixbuf);
167 priv->location_map_pixbuf = NULL;
168 }
169
170 if (priv->shadow_pixbuf) {
171 g_object_unref (priv->shadow_pixbuf);
172 priv->shadow_pixbuf = NULL;
173 }
174
175 if (priv->shadow_map_pixbuf) {
176 g_object_unref (priv->shadow_map_pixbuf);
177 priv->shadow_map_pixbuf = NULL;
178 }
179
180 G_OBJECT_CLASS (clock_map_parent_class)->finalize (g_obj);
181 }
182
183 void
clock_map_refresh(ClockMap * this)184 clock_map_refresh (ClockMap *this)
185 {
186 ClockMapPrivate *priv = clock_map_get_instance_private (this);
187 GtkWidget *widget = GTK_WIDGET (this);
188 GtkAllocation allocation;
189
190 gtk_widget_get_allocation (widget, &allocation);
191
192 /* Only do something if we have some space allocated.
193 * Note that 1x1 is not really some space... */
194 if (allocation.width <= 1 || allocation.height <= 1)
195 return;
196
197 /* Allocation changed => we reload the map */
198 if (priv->width != allocation.width ||
199 priv->height != allocation.height) {
200 if (priv->stock_map_pixbuf) {
201 g_object_unref (priv->stock_map_pixbuf);
202 priv->stock_map_pixbuf = NULL;
203 }
204
205 priv->width = allocation.width;
206 priv->height = allocation.height;
207 }
208
209 if (!priv->stock_map_pixbuf) {
210 priv->stock_map_pixbuf = gdk_pixbuf_new_from_resource_at_scale (CLOCK_RESOURCE_PATH "icons/clock-map.png",
211 priv->width, priv->height,
212 FALSE,
213 NULL);
214 }
215
216 clock_map_place_locations (this);
217
218 clock_map_display (this);
219 }
220
221 static gboolean
clock_map_draw(GtkWidget * this,cairo_t * cr)222 clock_map_draw (GtkWidget *this, cairo_t *cr)
223 {
224 ClockMapPrivate *priv = clock_map_get_instance_private (CLOCK_MAP(this));
225 int width, height;
226 GtkStyleContext *context;
227 GdkRGBA color;
228
229 context = gtk_widget_get_style_context (this);
230 gtk_style_context_get_color (context, GTK_STATE_FLAG_ACTIVE, &color);
231
232 if (!priv->shadow_map_pixbuf) {
233 g_warning ("Needed to refresh the map in expose event.");
234 clock_map_refresh (CLOCK_MAP (this));
235 }
236
237 width = gdk_pixbuf_get_width (priv->shadow_map_pixbuf);
238 height = gdk_pixbuf_get_height (priv->shadow_map_pixbuf);
239
240 gdk_cairo_set_source_pixbuf (cr, priv->shadow_map_pixbuf, 0, 0);
241 cairo_rectangle (cr, 0, 0, width, height);
242 cairo_paint (cr);
243
244 /* draw a simple outline */
245 cairo_rectangle (cr, 0.5, 0.5, width - 1, height - 1);
246 gdk_cairo_set_source_rgba (cr, &color);
247
248 cairo_set_line_width (cr, 1.0);
249 cairo_stroke (cr);
250
251 return FALSE;
252 }
253
254 static void
clock_map_get_preferred_width(GtkWidget * this,gint * minimum_width,gint * natural_width)255 clock_map_get_preferred_width (GtkWidget *this,
256 gint *minimum_width,
257 gint *natural_width)
258 {
259 *minimum_width = *natural_width = 250;
260 }
261
262 static void
clock_map_get_preferred_height(GtkWidget * this,gint * minimum_height,gint * natural_height)263 clock_map_get_preferred_height (GtkWidget *this,
264 gint *minimum_height,
265 gint *natural_height)
266 {
267 *minimum_height = *natural_height = 125;
268 }
269
270 static void
clock_map_size_allocate(GtkWidget * this,GtkAllocation * allocation)271 clock_map_size_allocate (GtkWidget *this, GtkAllocation *allocation)
272 {
273 ClockMapPrivate *priv = clock_map_get_instance_private (CLOCK_MAP(this));
274
275 if (GTK_WIDGET_CLASS (clock_map_parent_class)->size_allocate)
276 GTK_WIDGET_CLASS (clock_map_parent_class)->size_allocate (this, allocation);
277
278 if (priv->width != allocation->width ||
279 priv->height != allocation->height)
280 clock_map_refresh (CLOCK_MAP (this));
281 }
282
283 static void
clock_map_mark(ClockMap * this,gfloat latitude,gfloat longitude,gint mark)284 clock_map_mark (ClockMap *this, gfloat latitude, gfloat longitude, gint mark)
285 {
286 ClockMapPrivate *priv = clock_map_get_instance_private (this);
287 GdkPixbuf *marker = priv->location_marker_pixbuf[mark];
288 GdkPixbuf *partial = NULL;
289
290 int x, y;
291 int width, height;
292 int marker_width, marker_height;
293 int dest_x, dest_y, dest_width, dest_height;
294
295 width = gdk_pixbuf_get_width (priv->location_map_pixbuf);
296 height = gdk_pixbuf_get_height (priv->location_map_pixbuf);
297
298 x = (width / 2.0 + (width / 2.0) * longitude / 180.0);
299 y = (height / 2.0 - (height / 2.0) * latitude / 90.0);
300
301 marker_width = gdk_pixbuf_get_width (marker);
302 marker_height = gdk_pixbuf_get_height (marker);
303
304 dest_x = x - marker_width / 2;
305 dest_y = y - marker_height / 2;
306 dest_width = marker_width;
307 dest_height = marker_height;
308
309 /* create a small partial pixbuf if the mark is too close to
310 the north or south pole */
311 if (dest_y < 0) {
312 partial = gdk_pixbuf_new_subpixbuf
313 (marker, 0, dest_y + marker_height,
314 marker_width, -dest_y);
315
316 dest_y = 0.0;
317 marker_height = gdk_pixbuf_get_height (partial);
318 } else if (dest_y + dest_height > height) {
319 partial = gdk_pixbuf_new_subpixbuf
320 (marker, 0, 0, marker_width, height - dest_y);
321 marker_height = gdk_pixbuf_get_height (partial);
322 }
323
324 if (partial) {
325 marker = partial;
326 }
327
328 /* handle the cases where the marker needs to be placed across
329 the 180 degree longitude line */
330 if (dest_x < 0) {
331 /* split into our two pixbufs for the left and right edges */
332 GdkPixbuf *lhs = NULL;
333 GdkPixbuf *rhs = NULL;
334
335 lhs = gdk_pixbuf_new_subpixbuf
336 (marker, -dest_x, 0, marker_width + dest_x, marker_height);
337
338 gdk_pixbuf_composite (lhs, priv->location_map_pixbuf,
339 0, dest_y,
340 gdk_pixbuf_get_width (lhs),
341 gdk_pixbuf_get_height (lhs),
342 0, dest_y,
343 1.0, 1.0, GDK_INTERP_NEAREST, 0xFF);
344
345 rhs = gdk_pixbuf_new_subpixbuf
346 (marker, 0, 0, -dest_x, marker_height);
347
348 gdk_pixbuf_composite (rhs, priv->location_map_pixbuf,
349 width - gdk_pixbuf_get_width (rhs) - 1,
350 dest_y,
351 gdk_pixbuf_get_width (rhs),
352 gdk_pixbuf_get_height (rhs),
353 width - gdk_pixbuf_get_width (rhs) - 1,
354 dest_y,
355 1.0, 1.0, GDK_INTERP_NEAREST, 0xFF);
356
357 g_object_unref (lhs);
358 g_object_unref (rhs);
359 } else if (dest_x + dest_width > width) {
360 /* split into our two pixbufs for the left and right edges */
361 GdkPixbuf *lhs = NULL;
362 GdkPixbuf *rhs = NULL;
363
364 lhs = gdk_pixbuf_new_subpixbuf
365 (marker, width - dest_x, 0, marker_width - width + dest_x, marker_height);
366
367 gdk_pixbuf_composite (lhs, priv->location_map_pixbuf,
368 0, dest_y,
369 gdk_pixbuf_get_width (lhs),
370 gdk_pixbuf_get_height (lhs),
371 0, dest_y,
372 1.0, 1.0, GDK_INTERP_NEAREST, 0xFF);
373
374 rhs = gdk_pixbuf_new_subpixbuf
375 (marker, 0, 0, width - dest_x, marker_height);
376
377 gdk_pixbuf_composite (rhs, priv->location_map_pixbuf,
378 width - gdk_pixbuf_get_width (rhs) - 1,
379 dest_y,
380 gdk_pixbuf_get_width (rhs),
381 gdk_pixbuf_get_height (rhs),
382 width - gdk_pixbuf_get_width (rhs) - 1,
383 dest_y,
384 1.0, 1.0, GDK_INTERP_NEAREST, 0xFF);
385
386 g_object_unref (lhs);
387 g_object_unref (rhs);
388 } else {
389 gdk_pixbuf_composite (marker, priv->location_map_pixbuf,
390 dest_x, dest_y,
391 gdk_pixbuf_get_width (marker),
392 gdk_pixbuf_get_height (marker),
393 dest_x, dest_y,
394 1.0, 1.0, GDK_INTERP_NEAREST, 0xFF);
395 }
396
397 if (partial != NULL) {
398 g_object_unref (partial);
399 }
400 }
401
402 /**
403 * Return value: %TRUE if @loc can be placed on the map, %FALSE otherwise.
404 **/
405 static gboolean
clock_map_place_location(ClockMap * this,ClockLocation * loc,gboolean hilight)406 clock_map_place_location (ClockMap *this, ClockLocation *loc, gboolean hilight)
407 {
408 gfloat latitude, longitude;
409 gint marker;
410
411 clock_location_get_coords (loc, &latitude, &longitude);
412 /* 0/0 means unset, basically */
413 if (latitude == 0 && longitude == 0)
414 return FALSE;
415
416 if (hilight)
417 marker = MARKER_HILIGHT;
418 else if (clock_location_is_current (loc))
419 marker = MARKER_CURRENT;
420 else
421 marker = MARKER_NORMAL;
422
423 clock_map_mark (this, latitude, longitude, marker);
424
425 return TRUE;
426 }
427
428 static void
clock_map_place_locations(ClockMap * this)429 clock_map_place_locations (ClockMap *this)
430 {
431 ClockMapPrivate *priv = clock_map_get_instance_private (this);
432 GSList *locs;
433
434 if (priv->location_map_pixbuf) {
435 g_object_unref (priv->location_map_pixbuf);
436 priv->location_map_pixbuf = NULL;
437 }
438
439 priv->location_map_pixbuf = gdk_pixbuf_copy (priv->stock_map_pixbuf);
440
441 locs = NULL;
442 g_signal_emit (this, signals[NEED_LOCATIONS], 0, &locs);
443
444 while (locs) {
445 ClockLocation *loc = CLOCK_LOCATION (locs->data);
446 clock_map_place_location (this, loc, FALSE);
447 locs = locs->next;
448 }
449
450 #if 0
451 /* map_mark test suite for the edge cases */
452
453 /* points around longitude 180 */
454 clock_map_mark (this, 0.0, 180.0);
455 clock_map_mark (this, -15.0, -178.0);
456 clock_map_mark (this, -30.0, -176.0);
457 clock_map_mark (this, 15.0, 178.0);
458 clock_map_mark (this, 30.0, 176.0);
459
460 clock_map_mark (this, 90.0, 180.0);
461 clock_map_mark (this, -90.0, 180.0);
462
463 /* north pole & friends */
464 clock_map_mark (this, 90.0, 0.0);
465 clock_map_mark (this, 88.0, -15.0);
466 clock_map_mark (this, 92.0, 15.0);
467
468 /* south pole & friends */
469 clock_map_mark (this, -90.0, 0.0);
470 clock_map_mark (this, -88.0, -15.0);
471 clock_map_mark (this, -92.0, 15.0);
472 #endif
473 }
474
475 static void
clock_map_compute_vector(gdouble lat,gdouble lon,gdouble * vec)476 clock_map_compute_vector (gdouble lat, gdouble lon, gdouble *vec)
477 {
478 gdouble lat_rad, lon_rad;
479 lat_rad = lat * (M_PI/180.0);
480 lon_rad = lon * (M_PI/180.0);
481
482 vec[0] = sin(lon_rad) * cos(lat_rad);
483 vec[1] = sin(lat_rad);
484 vec[2] = cos(lon_rad) * cos(lat_rad);
485 }
486
487 static guchar
clock_map_is_sunlit(gdouble pos_lat,gdouble pos_long,gdouble sun_lat,gdouble sun_long)488 clock_map_is_sunlit (gdouble pos_lat, gdouble pos_long,
489 gdouble sun_lat, gdouble sun_long)
490 {
491 gdouble pos_vec[3];
492 gdouble sun_vec[3];
493 gdouble dot;
494
495 /* twilight */
496 gdouble epsilon = 0.01;
497
498 clock_map_compute_vector (pos_lat, pos_long, pos_vec);
499 clock_map_compute_vector (sun_lat, sun_long, sun_vec);
500
501 /* compute the dot product of the two */
502 dot = pos_vec[0]*sun_vec[0] + pos_vec[1]*sun_vec[1]
503 + pos_vec[2]*sun_vec[2];
504
505 if (dot > epsilon) {
506 return 0x00;
507 }
508
509 if (dot < -epsilon) {
510 return 0xFF;
511 }
512
513 return (guchar)(-128 * ((dot / epsilon) - 1));
514 }
515
516 static void
clock_map_render_shadow_pixbuf(GdkPixbuf * pixbuf)517 clock_map_render_shadow_pixbuf (GdkPixbuf *pixbuf)
518 {
519 int x, y;
520 int height, width;
521 int n_channels, rowstride;
522 guchar *pixels, *p;
523 gdouble sun_lat, sun_lon;
524 time_t now = time (NULL);
525
526 n_channels = gdk_pixbuf_get_n_channels (pixbuf);
527 rowstride = gdk_pixbuf_get_rowstride (pixbuf);
528 pixels = gdk_pixbuf_get_pixels (pixbuf);
529
530 width = gdk_pixbuf_get_width (pixbuf);
531 height = gdk_pixbuf_get_height (pixbuf);
532
533 sun_position (now, &sun_lat, &sun_lon);
534
535 for (y = 0; y < height; y++) {
536 gdouble lat = (height / 2.0 - y) / (height / 2.0) * 90.0;
537
538 for (x = 0; x < width; x++) {
539 guchar shade;
540 gdouble lon =
541 (x - width / 2.0) / (width / 2.0) * 180.0;
542
543 shade = clock_map_is_sunlit (lat, lon,
544 sun_lat, sun_lon);
545
546 p = pixels + y * rowstride + x * n_channels;
547 p[3] = shade;
548 }
549 }
550 }
551
552 static void
clock_map_render_shadow(ClockMap * this)553 clock_map_render_shadow (ClockMap *this)
554 {
555 ClockMapPrivate *priv = clock_map_get_instance_private (this);
556
557 if (priv->shadow_pixbuf) {
558 g_object_unref (priv->shadow_pixbuf);
559 }
560
561 priv->shadow_pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, TRUE, 8,
562 priv->width, priv->height);
563
564 /* Initialize to all shadow */
565 gdk_pixbuf_fill (priv->shadow_pixbuf, 0x6d9ccdff);
566
567 clock_map_render_shadow_pixbuf (priv->shadow_pixbuf);
568
569 if (priv->shadow_map_pixbuf) {
570 g_object_unref (priv->shadow_map_pixbuf);
571 }
572
573 priv->shadow_map_pixbuf = gdk_pixbuf_copy (priv->location_map_pixbuf);
574
575 gdk_pixbuf_composite (priv->shadow_pixbuf, priv->shadow_map_pixbuf,
576 0, 0, priv->width, priv->height,
577 0, 0, 1, 1, GDK_INTERP_NEAREST, 0x66);
578 }
579
580 static void
clock_map_display(ClockMap * this)581 clock_map_display (ClockMap *this)
582 {
583 ClockMapPrivate *priv = clock_map_get_instance_private (this);
584
585 if (priv->width > 0 || priv->height > 0)
586 clock_map_render_shadow (this);
587 gtk_widget_queue_draw (GTK_WIDGET (this));
588
589 time (&priv->last_refresh);
590 }
591
592 typedef struct {
593 ClockMap *map;
594 ClockLocation *location;
595 int count;
596 } BlinkData;
597
598 static gboolean
highlight(gpointer user_data)599 highlight (gpointer user_data)
600 {
601 BlinkData *data = user_data;
602
603 if (data->count == 6)
604 return FALSE;
605
606 if (data->count % 2 == 0) {
607 if (!clock_map_place_location (data->map,
608 data->location, TRUE))
609 return FALSE;
610 } else
611 clock_map_place_locations (data->map);
612 clock_map_display (data->map);
613
614 data->count++;
615
616 return TRUE;
617 }
618
619 static void
highlight_destroy(gpointer user_data)620 highlight_destroy (gpointer user_data)
621 {
622 BlinkData *data = user_data;
623 ClockMapPrivate *priv;
624
625 priv = clock_map_get_instance_private (data->map);
626 priv->highlight_timeout_id = 0;
627
628 g_object_unref (data->location);
629 g_free (data);
630 }
631
632 void
clock_map_blink_location(ClockMap * this,ClockLocation * loc)633 clock_map_blink_location (ClockMap *this, ClockLocation *loc)
634 {
635 BlinkData *data;
636 ClockMapPrivate *priv;
637
638 priv = clock_map_get_instance_private (this);
639
640 g_return_if_fail (IS_CLOCK_MAP (this));
641 g_return_if_fail (IS_CLOCK_LOCATION (loc));
642
643 data = g_new0 (BlinkData, 1);
644 data->map = this;
645 data->location = g_object_ref (loc);
646
647 if (priv->highlight_timeout_id) {
648 g_source_remove (priv->highlight_timeout_id);
649 clock_map_place_locations (this);
650 }
651
652 highlight (data);
653
654 priv->highlight_timeout_id = g_timeout_add_full (G_PRIORITY_DEFAULT_IDLE,
655 300, highlight, data,
656 highlight_destroy);
657 }
658
659 static gboolean
clock_map_needs_refresh(ClockMap * this)660 clock_map_needs_refresh (ClockMap *this)
661 {
662 ClockMapPrivate *priv = clock_map_get_instance_private (this);
663 time_t now_t;
664
665 time (&now_t);
666
667 /* refresh once per minute */
668 return (ABS (now_t - priv->last_refresh) >= 60);
669 }
670
671 void
clock_map_update_time(ClockMap * this)672 clock_map_update_time (ClockMap *this)
673 {
674
675 g_return_if_fail (IS_CLOCK_MAP (this));
676
677 if (!clock_map_needs_refresh (this)) {
678 return;
679 }
680
681 clock_map_display (this);
682 }
683