GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 22.4% 127 / 0 / 568
Functions: 25.0% 14 / 0 / 56
Branches: 12.0% 31 / 0 / 258

shumate/shumate-map.c
Line Branch Exec Source
1 /*
2 * Copyright (C) 2008-2009 Pierre-Luc Beaudoin <pierre-luc@pierlux.com>
3 * Copyright (C) 2010-2013 Jiri Techet <techet@gmail.com>
4 * Copyright (C) 2012,2020 Collabora Ltd. (https://www.collabora.com/)
5 * Copyright (C) 2019 Marcus Lundblad <ml@update.uu.se>
6 * Copyright (C) 2020 Corentin Noël <corentin.noel@collabora.com>
7 *
8 *
9 * This library is free software; you can redistribute it and/or
10 * modify it under the terms of the GNU Lesser General Public
11 * License as published by the Free Software Foundation; either
12 * version 2.1 of the License, or (at your option) any later version.
13 *
14 * This library is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 * Lesser General Public License for more details.
18 *
19 * You should have received a copy of the GNU Lesser General Public
20 * License along with this library; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
22 */
23
24 /**
25 * ShumateMap:
26 *
27 * The Map widget is a [class@Gtk.Widget] that show and allows interaction with
28 * the user.
29 *
30 * This is the base widget and doesn't have advanced features. You can check the
31 * [class@Shumate.SimpleMap] for a ready-to-use widget.
32 *
33 * By default, a [class@Shumate.Viewport] is created and can be accessed with
34 * [method@Shumate.Map.get_viewport].
35 *
36 * Unless created with [ctor@Shumate.Map.new_simple], the widget doesn't hold any
37 * layer and won't show anything. A [class@Shumate.Layer] can be added or removed
38 * using the [method@Shumate.Map.add_layer] or [method@Shumate.Map.remove_layer]
39 * methods.
40 *
41 * ## CSS nodes
42 *
43 * `ShumateMap` has a single CSS node with the name “map-view”.
44 */
45
46 #include "shumate-map.h"
47
48 #include "shumate.h"
49 #include "shumate-enum-types.h"
50 #include "shumate-inspector-page-private.h"
51 #include "shumate-inspector-settings-private.h"
52 #include "shumate-kinetic-scrolling-private.h"
53 #include "shumate-marshal.h"
54 #include "shumate-map-layer.h"
55 #include "shumate-map-source.h"
56 #include "shumate-map-source-registry.h"
57 #include "shumate-tile.h"
58 #include "shumate-license.h"
59 #include "shumate-location.h"
60 #include "shumate-viewport-private.h"
61
62 #include <glib.h>
63 #include <glib-object.h>
64 #include <gtk/gtk.h>
65 #include <math.h>
66
67 #define DECELERATION_FRICTION 4.0
68 #define ZOOM_ANIMATION_MS 200
69 #define ZOOM_COOLDOWN_MS 100
70 #define SCROLL_PIXELS_PER_LEVEL 50
71
72 enum
73 {
74 /* normal signals */
75 ANIMATION_COMPLETED,
76 LAST_SIGNAL
77 };
78
79 enum
80 {
81 PROP_ZOOM_ON_DOUBLE_CLICK = 1,
82 PROP_ANIMATE_ZOOM,
83 PROP_STATE,
84 PROP_GO_TO_DURATION,
85 PROP_VIEWPORT,
86 N_PROPERTIES
87 };
88
89 static guint signals[LAST_SIGNAL] = { 0, };
90
91 static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, };
92 static GQuark go_to_quark;
93
94 /* Between state values for go_to */
95 typedef struct
96 {
97 int64_t duration_us;
98 int64_t start_us;
99 double to_latitude;
100 double to_longitude;
101 double to_zoom;
102 double from_latitude;
103 double from_longitude;
104 double from_zoom;
105 guint tick_id;
106 gboolean zoom_animation : 1;
107 gboolean zoom_deceleration : 1;
108 } GoToContext;
109
110 typedef struct
111 {
112 ShumateKineticScrolling *kinetic_scrolling;
113 ShumateMap *map;
114 double start_lat;
115 double start_lon;
116 int64_t last_deceleration_time_us;
117 graphene_vec2_t direction;
118 } KineticScrollData;
119
120 struct _ShumateMap
121 {
122 GtkWidget parent_instance;
123
124 ShumateViewport *viewport;
125
126 gboolean zoom_on_double_click;
127 gboolean animate_zoom;
128
129 ShumateState state; /* View's global state */
130
131 GoToContext *goto_context;
132
133 guint deceleration_tick_id;
134
135 guint zoom_timeout;
136
137 guint go_to_duration;
138
139 double current_x;
140 double current_y;
141
142 double zoom_level_begin;
143 double rotate_begin;
144
145 double gesture_begin_lat;
146 double gesture_begin_lon;
147 double drag_begin_x;
148 double drag_begin_y;
149
150 gint64 last_zoom_update_time;
151 };
152
153
4/6
shumate_map_class_intern_init:
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
shumate_map_get_type:
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 7 taken 16 times.
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 7 not taken.
22 G_DEFINE_TYPE (ShumateMap, shumate_map, GTK_TYPE_WIDGET);
154
155 static double
156 ✗ positive_mod (double i, double n)
157 {
158 ✗ return fmod (fmod (i, n) + n, n);
159 }
160
161 static void
162 ✗ move_location_to_coords_calc (ShumateMap *self,
163 double *lat,
164 double *lon,
165 double x,
166 double y,
167 ShumateViewport *viewport)
168 {
169 ✗ ShumateMapSource *map_source = shumate_viewport_get_reference_map_source (viewport);
170 ✗ double zoom_level = shumate_viewport_get_zoom_level (viewport);
171 ✗ double tile_size, map_width, map_height;
172 ✗ double map_x, map_y;
173 ✗ double target_lat, target_lon;
174 ✗ double target_map_x, target_map_y;
175 ✗ double current_lat, current_lon;
176 ✗ double current_map_x, current_map_y;
177 ✗ double new_map_x, new_map_y;
178
179 ✗ if (map_source == NULL)
180 return;
181
182 ✗ tile_size = shumate_map_source_get_tile_size_at_zoom (map_source, zoom_level);
183 ✗ map_width = tile_size * shumate_map_source_get_column_count (map_source, zoom_level);
184 ✗ map_height = tile_size * shumate_map_source_get_row_count (map_source, zoom_level);
185
186 ✗ map_x = shumate_map_source_get_x (map_source, zoom_level, *lon);
187 ✗ map_y = shumate_map_source_get_y (map_source, zoom_level, *lat);
188
189 ✗ current_lat = shumate_location_get_latitude (SHUMATE_LOCATION (viewport));
190 ✗ current_lon = shumate_location_get_longitude (SHUMATE_LOCATION (viewport));
191 ✗ current_map_x = shumate_map_source_get_x (map_source, zoom_level, current_lon);
192 ✗ current_map_y = shumate_map_source_get_y (map_source, zoom_level, current_lat);
193
194 ✗ shumate_viewport_widget_coords_to_location (viewport, GTK_WIDGET (self), x, y, &target_lat, &target_lon);
195 ✗ target_map_x = shumate_map_source_get_x (map_source, zoom_level, target_lon);
196 ✗ target_map_y = shumate_map_source_get_y (map_source, zoom_level, target_lat);
197
198 ✗ new_map_x = positive_mod (current_map_x - (target_map_x - map_x), map_width);
199 ✗ new_map_y = positive_mod (current_map_y - (target_map_y - map_y), map_height);
200
201 ✗ *lat = shumate_map_source_get_latitude (map_source, zoom_level, new_map_y);
202 ✗ *lon = shumate_map_source_get_longitude (map_source, zoom_level, new_map_x);
203 }
204
205 static void
206 ✗ move_location_to_coords (ShumateMap *self,
207 double lat,
208 double lon,
209 double x,
210 double y)
211 {
212 ✗ move_location_to_coords_calc (self, &lat, &lon, x, y, self->viewport);
213 ✗ shumate_location_set_location (SHUMATE_LOCATION (self->viewport), lat, lon);
214 ✗ }
215
216 static void
217 ✗ move_viewport_from_pixel_offset (ShumateMap *self,
218 double latitude,
219 double longitude,
220 double offset_x,
221 double offset_y)
222 {
223 ✗ ShumateMapSource *map_source;
224 ✗ double x, y;
225 ✗ double lat, lon;
226
227 ✗ g_assert (SHUMATE_IS_MAP (self));
228
229 ✗ map_source = shumate_viewport_get_reference_map_source (self->viewport);
230 ✗ if (!map_source)
231 return;
232
233 ✗ shumate_viewport_location_to_widget_coords (self->viewport, GTK_WIDGET (self), latitude, longitude, &x, &y);
234
235 ✗ x -= offset_x;
236 ✗ y -= offset_y;
237
238 ✗ shumate_viewport_widget_coords_to_location (self->viewport, GTK_WIDGET (self), x, y, &lat, &lon);
239
240 ✗ lat = fmod (lat + 90, 180) - 90;
241 ✗ lon = fmod (lon + 180, 360) - 180;
242
243 ✗ shumate_location_set_location (SHUMATE_LOCATION (self->viewport), lat, lon);
244 }
245
246 static void
247 ✗ cancel_deceleration (ShumateMap *self)
248 {
249 ✗ if (self->deceleration_tick_id > 0)
250 {
251 ✗ gtk_widget_remove_tick_callback (GTK_WIDGET (self), self->deceleration_tick_id);
252 ✗ self->deceleration_tick_id = 0;
253 }
254 ✗ }
255
256 static gboolean
257 ✗ view_deceleration_tick_cb (GtkWidget *widget,
258 GdkFrameClock *frame_clock,
259 gpointer user_data)
260 {
261 ✗ KineticScrollData *data = user_data;
262 ✗ ShumateMap *map = data->map;
263 ✗ int64_t current_time_us;
264 ✗ double elapsed_us;
265 ✗ double position;
266
267 ✗ g_assert (SHUMATE_IS_MAP (map));
268
269 ✗ current_time_us = gdk_frame_clock_get_frame_time (frame_clock);
270 ✗ elapsed_us = current_time_us - data->last_deceleration_time_us;
271
272 /* The frame clock can sometimes fire immediately after adding a tick callback,
273 * in which case no time has passed, making it impossible to calculate the
274 * kinetic factor. If this is the case, wait for the next tick.
275 */
276 ✗ if (G_APPROX_VALUE (elapsed_us, 0.0, FLT_EPSILON))
277 return G_SOURCE_CONTINUE;
278
279 ✗ data->last_deceleration_time_us = current_time_us;
280
281 ✗ if (data->kinetic_scrolling &&
282 ✗ shumate_kinetic_scrolling_tick (data->kinetic_scrolling, elapsed_us, &position))
283 ✗ {
284 ✗ graphene_vec2_t new_positions;
285
286 ✗ graphene_vec2_init (&new_positions, position, position);
287 ✗ graphene_vec2_multiply (&new_positions, &data->direction, &new_positions);
288
289 ✗ move_viewport_from_pixel_offset (map,
290 data->start_lat,
291 data->start_lon,
292 ✗ graphene_vec2_get_x (&new_positions),
293 ✗ graphene_vec2_get_y (&new_positions));
294 }
295 else
296 {
297 ✗ g_clear_pointer (&data->kinetic_scrolling, shumate_kinetic_scrolling_free);
298 }
299
300 ✗ if (!data->kinetic_scrolling)
301 {
302 ✗ cancel_deceleration (map);
303 ✗ return G_SOURCE_REMOVE;
304 }
305
306 return G_SOURCE_CONTINUE;
307 }
308
309
310 static void
311 ✗ kinetic_scroll_data_free (KineticScrollData *data)
312 {
313 ✗ if (data == NULL)
314 return;
315
316 ✗ g_clear_pointer (&data->kinetic_scrolling, shumate_kinetic_scrolling_free);
317 ✗ g_free (data);
318 }
319
320 static void
321 ✗ start_deceleration (ShumateMap *self,
322 double h_velocity,
323 double v_velocity)
324 {
325 ✗ GdkFrameClock *frame_clock;
326 ✗ KineticScrollData *data;
327 ✗ graphene_vec2_t velocity;
328
329 ✗ g_assert (self->deceleration_tick_id == 0);
330
331 ✗ frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self));
332
333 ✗ graphene_vec2_init (&velocity, h_velocity, v_velocity);
334
335 ✗ data = g_new0 (KineticScrollData, 1);
336 ✗ data->map = self;
337 ✗ data->last_deceleration_time_us = gdk_frame_clock_get_frame_time (frame_clock);
338 ✗ data->start_lat = shumate_location_get_latitude (SHUMATE_LOCATION (self->viewport));
339 ✗ data->start_lon = shumate_location_get_longitude (SHUMATE_LOCATION (self->viewport));
340 ✗ graphene_vec2_normalize (&velocity, &data->direction);
341 ✗ data->kinetic_scrolling =
342 ✗ shumate_kinetic_scrolling_new (DECELERATION_FRICTION,
343 ✗ graphene_vec2_length (&velocity));
344
345 ✗ self->deceleration_tick_id =
346 ✗ gtk_widget_add_tick_callback (GTK_WIDGET (self),
347 view_deceleration_tick_cb,
348 data,
349 (GDestroyNotify) kinetic_scroll_data_free);
350 ✗ }
351
352 static inline double
353 ✗ ease_in_out_quad (double p)
354 {
355 ✗ p = 2.0 * p;
356 ✗ if (p < 1.0)
357 ✗ return 0.5 * p * p;
358
359 ✗ p -= 1.0;
360 ✗ return -0.5 * (p * (p - 2) - 1);
361 }
362
363 static inline double
364 ✗ ease_out_quad (double p)
365 {
366 ✗ return 1 - (1 - p) * (1 - p);
367 }
368
369 static inline int64_t
370 ✗ ms_to_us (int64_t ms)
371 {
372 ✗ return ms * 1000;
373 }
374
375 static gboolean
376 ✗ go_to_tick_cb (GtkWidget *widget,
377 GdkFrameClock *frame_clock,
378 gpointer user_data)
379 {
380 ✗ ShumateMap *self = SHUMATE_MAP (widget);
381 ✗ GoToContext *ctx;
382 ✗ int64_t now_us;
383 ✗ double latitude, longitude;
384 ✗ double progress;
385 ✗ double current_zoom;
386
387 ✗ g_assert (SHUMATE_IS_MAP (widget));
388 ✗ g_assert (user_data == NULL);
389
390 ✗ ctx = self->goto_context;
391
392 ✗ g_assert (ctx != NULL);
393 ✗ g_assert (ctx->duration_us >= 0);
394
395 ✗ now_us = g_get_monotonic_time ();
396
397 ✗ if (now_us >= ctx->start_us + ctx->duration_us)
398 {
399 ✗ shumate_location_set_location (SHUMATE_LOCATION (self->viewport),
400 ctx->to_latitude,
401 ctx->to_longitude);
402 ✗ shumate_viewport_set_zoom_level (self->viewport, ctx->to_zoom);
403 ✗ shumate_map_stop_go_to (SHUMATE_MAP (widget));
404 ✗ return G_SOURCE_REMOVE;
405 }
406
407 ✗ progress = (now_us - ctx->start_us) / (double) ctx->duration_us;
408 ✗ g_assert (progress >= 0.0 && progress <= 1.0);
409
410 /* Apply the ease function to the progress itself */
411 ✗ if (ctx->zoom_deceleration)
412 ✗ progress = ease_out_quad (progress);
413 ✗ else if (!ctx->zoom_animation)
414 ✗ progress = ease_in_out_quad (progress);
415
416 /* Interpolate zoom level */
417 ✗ current_zoom = ctx->from_zoom + (ctx->to_zoom - ctx->from_zoom) * progress;
418 ✗ shumate_viewport_set_zoom_level (self->viewport, current_zoom);
419
420 /* If we're zooming, we need to adjust for that in the progress, otherwise
421 * the animation will speed up at higher zoom levels. */
422 ✗ if (ctx->to_zoom != ctx->from_zoom)
423 ✗ progress = (pow (2, -ctx->from_zoom) - pow (2, -current_zoom))
424 ✗ / (pow (2, -ctx->from_zoom) - pow (2, -ctx->to_zoom));
425
426 /* Since progress already follows the easing curve, a simple LERP is guaranteed
427 * to follow it too.
428 */
429 ✗ latitude = ctx->from_latitude + (ctx->to_latitude - ctx->from_latitude) * progress;
430 ✗ longitude = ctx->from_longitude + (ctx->to_longitude - ctx->from_longitude) * progress;
431
432 ✗ shumate_location_set_location (SHUMATE_LOCATION (self->viewport), latitude, longitude);
433
434 ✗ return G_SOURCE_CONTINUE;
435 }
436
437 static void
438 ✗ on_drag_gesture_drag_begin (ShumateMap *self,
439 double start_x,
440 double start_y,
441 GtkGestureDrag *gesture)
442 {
443
444 ✗ if (g_get_monotonic_time() - self-> last_zoom_update_time < ZOOM_COOLDOWN_MS * 1000)
445 return;
446
447 ✗ g_assert (SHUMATE_IS_MAP (self));
448
449 ✗ cancel_deceleration (self);
450
451 ✗ self->drag_begin_x = start_x;
452 ✗ self->drag_begin_y = start_y;
453
454 ✗ shumate_viewport_widget_coords_to_location (self->viewport, GTK_WIDGET (self),
455 start_x, start_y,
456 &self->gesture_begin_lat,
457 &self->gesture_begin_lon);
458
459 ✗ gtk_widget_set_cursor_from_name (GTK_WIDGET (self), "grabbing");
460 }
461
462 static void
463 ✗ on_drag_gesture_drag_update (ShumateMap *self,
464 double offset_x,
465 double offset_y,
466 GtkGestureDrag *gesture)
467 {
468
469 ✗ if (g_get_monotonic_time() - self-> last_zoom_update_time < ZOOM_COOLDOWN_MS * 1000)
470 return;
471
472 ✗ move_location_to_coords (self,
473 self->gesture_begin_lat,
474 self->gesture_begin_lon,
475 ✗ self->drag_begin_x + offset_x,
476 ✗ self->drag_begin_y + offset_y);
477 }
478
479 static void
480 ✗ on_drag_gesture_drag_end (ShumateMap *self,
481 double offset_x,
482 double offset_y,
483 GtkGestureDrag *gesture)
484 {
485 ✗ g_assert (SHUMATE_IS_MAP (self));
486
487 ✗ gtk_widget_set_cursor_from_name (GTK_WIDGET (self), "grab");
488
489 ✗ self->gesture_begin_lon = 0;
490 ✗ self->gesture_begin_lat = 0;
491 ✗ }
492
493 static void
494 ✗ view_swipe_cb (GtkGestureSwipe *swipe_gesture,
495 double velocity_x,
496 double velocity_y,
497 ShumateMap *self)
498 {
499 ✗ start_deceleration (self, velocity_x, velocity_y);
500 ✗ }
501
502 static void
503 1 set_zoom_level (ShumateMap *self,
504 double zoom_level,
505 double animate)
506 {
507 1 ShumateMapSource *map_source;
508 1 double lat, lon;
509
510 1 g_object_freeze_notify (G_OBJECT (self->viewport));
511
512 1 map_source = shumate_viewport_get_reference_map_source (self->viewport);
513
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 17 taken 1 time.
1 if (map_source)
514 ✗ shumate_viewport_widget_coords_to_location (self->viewport,
515 GTK_WIDGET (self),
516 self->current_x, self->current_y,
517 &lat, &lon);
518
519 ✗ if (map_source)
520 {
521 ✗ g_autoptr(ShumateViewport) new_viewport = shumate_viewport_copy (self->viewport);
522 ✗ shumate_viewport_set_zoom_level (new_viewport, zoom_level);
523 ✗ move_location_to_coords_calc (self, &lat, &lon, self->current_x, self->current_y, new_viewport);
524
525 ✗ shumate_map_go_to_full_with_duration (self,
526 lat,
527 lon,
528 zoom_level,
529 ✗ self->animate_zoom && animate ? ZOOM_ANIMATION_MS : 0);
530
531 ✗ if (self->goto_context != NULL)
532 ✗ self->goto_context->zoom_animation = TRUE;
533 }
534 else
535 1 shumate_viewport_set_zoom_level (self->viewport, zoom_level);
536
537 1 g_object_thaw_notify (G_OBJECT (self->viewport));
538 1 }
539
540 static gboolean
541 ✗ on_scroll_controller_scroll (ShumateMap *self,
542 double dx,
543 double dy,
544 GtkEventControllerScroll *controller)
545 {
546 ✗ double zoom_level = shumate_viewport_get_zoom_level (self->viewport);
547
548 ✗ if (self->goto_context != NULL && self->goto_context->zoom_animation)
549 ✗ zoom_level = self->goto_context->to_zoom;
550
551 ✗ if (gtk_event_controller_scroll_get_unit (controller) == GDK_SCROLL_UNIT_SURFACE)
552 /* Smooth scrolling with a touchpad or similar device */
553 ✗ set_zoom_level (self, zoom_level - dy / SCROLL_PIXELS_PER_LEVEL, FALSE);
554 else
555 {
556 ✗ zoom_level -= dy / 5;
557 ✗ if (fabs(dy) == 1)
558 /* Traditional discrete mouse, snap to the nearest 1/5 of a zoom level */
559 ✗ set_zoom_level (self, roundf (zoom_level * 5) / 5, TRUE);
560 else
561 /* Smooth scrolling using a mouse.
562 *
563 * Various smooth-scrolling mice behave like "discrete" mice,
564 * while emitting fractions of a scroll at the same time.
565 * Do not round their events, or most of the scrolling gets ignored.*/
566 ✗ set_zoom_level (self, zoom_level, TRUE);
567 }
568
569 ✗ return TRUE;
570 }
571
572 static gboolean
573 ✗ on_scroll_controller_decelerate (ShumateMap *self,
574 double vel_x,
575 double vel_y,
576 GtkEventControllerScroll *controller)
577 {
578 ✗ double zoom_level = shumate_viewport_get_zoom_level (self->viewport);
579
580 ✗ if (self->goto_context != NULL && self->goto_context->zoom_animation)
581 ✗ zoom_level = self->goto_context->to_zoom;
582
583 ✗ set_zoom_level (self, zoom_level - vel_y / SCROLL_PIXELS_PER_LEVEL / ZOOM_ANIMATION_MS, TRUE);
584 ✗ if (self->goto_context != NULL)
585 ✗ self->goto_context->zoom_deceleration = TRUE;
586
587 ✗ return TRUE;
588 }
589
590 static void
591 ✗ on_zoom_gesture_begin (ShumateMap *self,
592 GdkEventSequence *seq,
593 GtkGestureZoom *zoom)
594 {
595 ✗ double zoom_level = shumate_viewport_get_zoom_level (self->viewport);
596 ✗ double x, y;
597
598 ✗ self->last_zoom_update_time = g_get_monotonic_time();
599
600 ✗ gtk_gesture_set_state (GTK_GESTURE (zoom), GTK_EVENT_SEQUENCE_CLAIMED);
601 ✗ cancel_deceleration (self);
602
603 ✗ self->zoom_level_begin = zoom_level;
604
605 ✗ gtk_gesture_get_bounding_box_center (GTK_GESTURE (zoom), &x, &y);
606 ✗ shumate_viewport_widget_coords_to_location (self->viewport, GTK_WIDGET (self),
607 x, y,
608 &self->gesture_begin_lat,
609 &self->gesture_begin_lon);
610 ✗ }
611
612 static void
613 ✗ on_zoom_gesture_update (ShumateMap *self,
614 GdkEventSequence *seq,
615 GtkGestureZoom *zoom)
616 {
617 ✗ double x, y;
618 ✗ double scale = gtk_gesture_zoom_get_scale_delta (zoom);
619
620 ✗ gtk_gesture_get_bounding_box_center (GTK_GESTURE (zoom), &x, &y);
621 ✗ shumate_viewport_set_zoom_level (self->viewport, self->zoom_level_begin + log (scale) / G_LN2);
622 ✗ move_location_to_coords (self, self->gesture_begin_lat, self->gesture_begin_lon, x, y);
623
624 ✗ self->last_zoom_update_time = g_get_monotonic_time();
625 ✗ }
626
627 static void
628 ✗ on_rotate_gesture_begin (ShumateMap *self,
629 GdkEventSequence *seq,
630 GtkGestureRotate *rotate)
631 {
632 ✗ double rotation = shumate_viewport_get_rotation (self->viewport);
633
634 ✗ self->last_zoom_update_time = g_get_monotonic_time();
635
636 ✗ gtk_gesture_set_state (GTK_GESTURE (rotate), GTK_EVENT_SEQUENCE_CLAIMED);
637 ✗ cancel_deceleration (self);
638
639 ✗ self->rotate_begin = rotation;
640 ✗ }
641
642 static void
643 ✗ on_rotate_gesture_update (ShumateMap *self,
644 GdkEventSequence *seq,
645 GtkGestureRotate *rotate)
646 {
647 ✗ double rotation;
648 ✗ double x, y;
649
650 ✗ rotation = gtk_gesture_rotate_get_angle_delta (rotate) + self->rotate_begin;
651
652 /* snap to due north */
653 ✗ if (fabs (fmod (rotation - 0.25, G_PI * 2)) < 0.5)
654 ✗ rotation = 0.0;
655
656 ✗ shumate_viewport_set_rotation (self->viewport, rotation);
657 ✗ gtk_gesture_get_bounding_box_center (GTK_GESTURE (rotate), &x, &y);
658 ✗ move_location_to_coords (self, self->gesture_begin_lat, self->gesture_begin_lon, x, y);
659
660 ✗ self->last_zoom_update_time = g_get_monotonic_time();
661 ✗ }
662
663 static void
664 2 on_click_gesture_pressed (ShumateMap *self,
665 int n_press,
666 double x,
667 double y,
668 GtkGestureClick *click)
669 {
670
3/4
✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 6 not taken.
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 6 taken 1 time.
2 if (n_press == 2 && self->zoom_on_double_click == TRUE)
671 {
672 1 double zoom_level = shumate_viewport_get_zoom_level (self->viewport);
673 1 self->current_x = x;
674 1 self->current_y = y;
675 1 set_zoom_level (self, zoom_level + 1, TRUE);
676 }
677 2 }
678
679 static void
680 ✗ on_motion_controller_motion (ShumateMap *self,
681 double x,
682 double y,
683 GtkEventControllerMotion *controller)
684 {
685 ✗ self->current_x = x;
686 ✗ self->current_y = y;
687 ✗ }
688
689 static void
690 ✗ on_arrow_key (GtkWidget *widget,
691 const char *action,
692 GVariant *args)
693 {
694 ✗ ShumateMap *self = SHUMATE_MAP (widget);
695 ✗ int dx, dy;
696 ✗ double lat, lon;
697
698 ✗ g_variant_get (args, "(ii)", &dx, &dy);
699
700 ✗ shumate_viewport_widget_coords_to_location (self->viewport, widget, dx * 25, dy * 25, &lat, &lon);
701 ✗ move_location_to_coords (self, lat, lon, 0, 0);
702 ✗ }
703
704 static void
705 ✗ shumate_map_get_property (GObject *object,
706 guint prop_id,
707 GValue *value,
708 GParamSpec *pspec)
709 {
710 ✗ ShumateMap *self = SHUMATE_MAP (object);
711
712 ✗ switch (prop_id)
713 {
714 ✗ case PROP_ZOOM_ON_DOUBLE_CLICK:
715 ✗ g_value_set_boolean (value, self->zoom_on_double_click);
716 ✗ break;
717
718 ✗ case PROP_ANIMATE_ZOOM:
719 ✗ g_value_set_boolean (value, self->animate_zoom);
720 ✗ break;
721
722 ✗ case PROP_STATE:
723 ✗ g_value_set_enum (value, self->state);
724 ✗ break;
725
726 ✗ case PROP_GO_TO_DURATION:
727 ✗ g_value_set_uint (value, self->go_to_duration);
728 ✗ break;
729
730 ✗ case PROP_VIEWPORT:
731 ✗ g_value_set_object (value, self->viewport);
732 ✗ break;
733
734 ✗ default:
735 ✗ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
736 }
737 ✗ }
738
739
740 static void
741 ✗ shumate_map_set_property (GObject *object,
742 guint prop_id,
743 const GValue *value,
744 GParamSpec *pspec)
745 {
746 ✗ ShumateMap *self = SHUMATE_MAP (object);
747
748 ✗ switch (prop_id)
749 {
750 ✗ case PROP_ZOOM_ON_DOUBLE_CLICK:
751 ✗ shumate_map_set_zoom_on_double_click (self, g_value_get_boolean (value));
752 ✗ break;
753
754 ✗ case PROP_ANIMATE_ZOOM:
755 ✗ shumate_map_set_animate_zoom (self, g_value_get_boolean (value));
756 ✗ break;
757
758 ✗ case PROP_GO_TO_DURATION:
759 ✗ shumate_map_set_go_to_duration (self, g_value_get_uint (value));
760 ✗ break;
761
762 ✗ default:
763 ✗ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
764 }
765 ✗ }
766
767
768 static void
769 ✗ shumate_map_dispose (GObject *object)
770 {
771 ✗ ShumateMap *self = SHUMATE_MAP (object);
772 ✗ GtkWidget *child;
773
774 ✗ if (self->goto_context != NULL)
775 ✗ shumate_map_stop_go_to (self);
776
777 ✗ while ((child = gtk_widget_get_first_child (GTK_WIDGET (object))))
778 ✗ gtk_widget_unparent (child);
779
780 ✗ g_clear_object (&self->viewport);
781
782 ✗ g_clear_handle_id (&self->zoom_timeout, g_source_remove);
783
784 ✗ G_OBJECT_CLASS (shumate_map_parent_class)->dispose (object);
785 ✗ }
786
787 static void
788 ✗ shumate_map_snapshot (GtkWidget *widget,
789 GtkSnapshot *snapshot)
790 {
791 ✗ ShumateMap *self = SHUMATE_MAP (widget);
792 ✗ ShumateInspectorSettings *settings = shumate_inspector_settings_get_default ();
793 ✗ ShumateMapSource *map_source = shumate_viewport_get_reference_map_source (self->viewport);
794 ✗ int width, height;
795
796 ✗ GTK_WIDGET_CLASS (shumate_map_parent_class)->snapshot (GTK_WIDGET (self), snapshot);
797
798 ✗ if (shumate_inspector_settings_get_show_debug_overlay (settings))
799 {
800 ✗ g_autoptr(GString) all_debug_text = g_string_new ("");
801 ✗ PangoContext *context = gtk_widget_create_pango_context (GTK_WIDGET (self));
802 ✗ g_autoptr(PangoLayout) layout = NULL;
803 ✗ double lat = shumate_location_get_latitude (SHUMATE_LOCATION (self->viewport));
804 ✗ double lon = shumate_location_get_longitude (SHUMATE_LOCATION (self->viewport));
805 ✗ double zoom = shumate_viewport_get_zoom_level (self->viewport);
806 ✗ double rot = shumate_viewport_get_rotation (self->viewport);
807
808 ✗ g_string_append (all_debug_text, "<tt>");
809 ✗ g_string_append_printf (all_debug_text,
810 "lat = %9.5f, lon = %10.5f\nzoom = %5.2f, rot = %5.1f\n",
811 ✗ lat, lon, zoom, rot * 180 / G_PI);
812
813 ✗ if (map_source != NULL)
814 ✗ g_string_append_printf (all_debug_text, "tile size = %4dpx (%7.2f)\n",
815 shumate_map_source_get_tile_size (map_source),
816 shumate_map_source_get_tile_size_at_zoom (map_source, zoom));
817
818 ✗ g_string_append (all_debug_text, "\n");
819
820 ✗ for (GtkWidget *w = gtk_widget_get_first_child (GTK_WIDGET (self));
821 ✗ w != NULL;
822 ✗ w = gtk_widget_get_next_sibling (w))
823 {
824 ✗ ShumateLayer *layer = SHUMATE_LAYER (w);
825 ✗ g_string_append_printf (all_debug_text, "<b>%s</b>\n", G_OBJECT_CLASS_NAME (G_OBJECT_GET_CLASS (layer)));
826 ✗ if (SHUMATE_LAYER_GET_CLASS (layer)->get_debug_text != NULL)
827 {
828 ✗ g_autofree char *debug_text = SHUMATE_LAYER_GET_CLASS (layer)->get_debug_text (layer);
829 ✗ if (debug_text != NULL)
830 {
831 ✗ g_string_append (all_debug_text, debug_text);
832 ✗ g_string_append (all_debug_text, "\n");
833 }
834 }
835 }
836
837 ✗ g_string_append (all_debug_text, "</tt>");
838
839 ✗ layout = pango_layout_new (context);
840 ✗ pango_layout_set_markup (layout, all_debug_text->str, -1);
841 ✗ pango_layout_set_width (layout, gtk_widget_get_width (GTK_WIDGET (self)) * PANGO_SCALE);
842
843 ✗ pango_layout_get_pixel_size (layout, &width, &height);
844 ✗ gtk_snapshot_append_color (snapshot, &(GdkRGBA){1, 1, 1, 0.7}, &GRAPHENE_RECT_INIT (0, 0, width, height));
845
846 ✗ gtk_snapshot_append_layout (snapshot, layout, &(GdkRGBA){0, 0, 0, 1});
847
848 ✗ g_object_unref (context);
849 }
850 ✗ }
851
852 static void
853 2 shumate_map_class_init (ShumateMapClass *klass)
854 {
855 2 GObjectClass *object_class = G_OBJECT_CLASS (klass);
856 2 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
857
858 2 object_class->dispose = shumate_map_dispose;
859 2 object_class->get_property = shumate_map_get_property;
860 2 object_class->set_property = shumate_map_set_property;
861
862 2 widget_class->snapshot = shumate_map_snapshot;
863
864 /**
865 * ShumateMap:zoom-on-double-click:
866 *
867 * Should the view zoom in and recenter when the user double click on the map.
868 */
869 4 obj_properties[PROP_ZOOM_ON_DOUBLE_CLICK] =
870 2 g_param_spec_boolean ("zoom-on-double-click", NULL, NULL,
871 TRUE,
872 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
873
874 /**
875 * ShumateMap:animate-zoom:
876 *
877 * Animate zoom change when zooming in/out.
878 */
879 4 obj_properties[PROP_ANIMATE_ZOOM] =
880 2 g_param_spec_boolean ("animate-zoom", NULL, NULL,
881 TRUE,
882 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
883
884 /**
885 * ShumateMap:state:
886 *
887 * The view's global state. Useful to inform using if the view is busy loading
888 * tiles or not.
889 */
890 4 obj_properties[PROP_STATE] =
891 2 g_param_spec_enum ("state", NULL, NULL,
892 SHUMATE_TYPE_STATE,
893 SHUMATE_STATE_NONE,
894 G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
895
896 /**
897 * ShumateMap:go-to-duration:
898 *
899 * The duration of an animation when going to a location, in milliseconds.
900 * A value of 0 means that the duration is calculated automatically for you.
901 *
902 * Please note that animation of #shumate_map_ensure_visible also
903 * involves a 'go-to' animation.
904 *
905 */
906 4 obj_properties[PROP_GO_TO_DURATION] =
907 2 g_param_spec_uint ("go-to-duration", NULL, NULL,
908 0, G_MAXUINT, 0,
909 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
910
911 /**
912 * ShumateMap:viewport:
913 *
914 * The viewport, which contains information about the center, rotation, zoom,
915 * etc. of the map.
916 */
917 4 obj_properties[PROP_VIEWPORT] =
918 2 g_param_spec_object ("viewport", NULL, NULL,
919 SHUMATE_TYPE_VIEWPORT,
920 G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
921
922 2 g_object_class_install_properties (object_class,
923 N_PROPERTIES,
924 obj_properties);
925
926 /**
927 * ShumateMap::animation-completed:
928 *
929 * The #ShumateMap::animation-completed signal is emitted when any animation in the view
930 * ends. This is a detailed signal. For example, if you want to be signaled
931 * only for go-to animation, you should connect to
932 * "animation-completed::go-to". And for zoom, connect to "animation-completed::zoom".
933 */
934 4 signals[ANIMATION_COMPLETED] =
935 2 g_signal_new ("animation-completed",
936 G_OBJECT_CLASS_TYPE (object_class),
937 G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
938 0, NULL, NULL,
939 g_cclosure_marshal_VOID__OBJECT,
940 G_TYPE_NONE,
941 0);
942
943 /* Arrow keys */
944 2 gtk_widget_class_install_action (widget_class, "pan", "(ii)", on_arrow_key);
945 2 gtk_widget_class_add_binding_action (widget_class,
946 GDK_KEY_Left, 0,
947 "pan",
948 "(ii)", -1, 0);
949 2 gtk_widget_class_add_binding_action (widget_class,
950 GDK_KEY_Right, 0,
951 "pan",
952 "(ii)", 1, 0);
953 2 gtk_widget_class_add_binding_action (widget_class,
954 GDK_KEY_Up, 0,
955 "pan",
956 "(ii)", 0, -1);
957 2 gtk_widget_class_add_binding_action (widget_class,
958 GDK_KEY_Down, 0,
959 "pan",
960 "(ii)", 0, 1);
961
962 2 gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
963 2 gtk_widget_class_set_css_name (widget_class, "map-view");
964
965 2 go_to_quark = g_quark_from_static_string ("go-to");
966
967 2 shumate_inspector_page_ensure_registered ();
968 2 }
969
970 static void
971 2 shumate_map_init (ShumateMap *self)
972 {
973 2 ShumateInspectorSettings *settings = shumate_inspector_settings_get_default ();
974 2 GtkGesture *drag_gesture;
975 2 GtkEventController *scroll_controller;
976 2 GtkEventController *motion_controller;
977 2 GtkGesture *swipe_gesture;
978 2 GtkGesture *zoom_gesture;
979 2 GtkGesture *rotate_gesture;
980 2 GtkGesture *click_gesture;
981
982 2 self->viewport = shumate_viewport_new ();
983 2 self->zoom_on_double_click = TRUE;
984 2 self->animate_zoom = TRUE;
985 2 self->state = SHUMATE_STATE_NONE;
986 2 self->goto_context = NULL;
987 2 self->go_to_duration = 0;
988
989 2 g_signal_connect_object (settings, "notify::show-debug-overlay", G_CALLBACK (gtk_widget_queue_draw), self, G_CONNECT_SWAPPED);
990
991 2 gtk_widget_set_cursor_from_name (GTK_WIDGET (self), "grab");
992
993 2 drag_gesture = gtk_gesture_drag_new ();
994 2 g_signal_connect_swapped (drag_gesture, "drag-begin", G_CALLBACK (on_drag_gesture_drag_begin), self);
995 2 g_signal_connect_swapped (drag_gesture, "drag-update", G_CALLBACK (on_drag_gesture_drag_update), self);
996 2 g_signal_connect_swapped (drag_gesture, "drag-end", G_CALLBACK (on_drag_gesture_drag_end), self);
997 2 gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (drag_gesture));
998
999 2 swipe_gesture = gtk_gesture_swipe_new ();
1000 2 g_signal_connect (swipe_gesture, "swipe", G_CALLBACK (view_swipe_cb), self);
1001 2 gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (swipe_gesture));
1002
1003 2 scroll_controller = gtk_event_controller_scroll_new (GTK_EVENT_CONTROLLER_SCROLL_VERTICAL|GTK_EVENT_CONTROLLER_SCROLL_KINETIC);
1004 2 g_signal_connect_swapped (scroll_controller, "scroll", G_CALLBACK (on_scroll_controller_scroll), self);
1005 2 g_signal_connect_swapped (scroll_controller, "decelerate", G_CALLBACK (on_scroll_controller_decelerate), self);
1006 2 gtk_widget_add_controller (GTK_WIDGET (self), scroll_controller);
1007
1008 2 zoom_gesture = gtk_gesture_zoom_new ();
1009 2 g_signal_connect_swapped (zoom_gesture, "begin", G_CALLBACK (on_zoom_gesture_begin), self);
1010 2 g_signal_connect_swapped (zoom_gesture, "update", G_CALLBACK (on_zoom_gesture_update), self);
1011 2 gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (zoom_gesture));
1012
1013 2 motion_controller = gtk_event_controller_motion_new ();
1014 2 g_signal_connect_swapped (motion_controller, "motion", G_CALLBACK (on_motion_controller_motion), self);
1015 2 gtk_widget_add_controller (GTK_WIDGET (self), motion_controller);
1016
1017 2 rotate_gesture = gtk_gesture_rotate_new ();
1018 2 g_signal_connect_swapped (rotate_gesture, "begin", G_CALLBACK (on_rotate_gesture_begin), self);
1019 2 g_signal_connect_swapped (rotate_gesture, "update", G_CALLBACK (on_rotate_gesture_update), self);
1020 2 gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (rotate_gesture));
1021
1022 2 gtk_gesture_group (zoom_gesture, rotate_gesture);
1023
1024 2 click_gesture = gtk_gesture_click_new ();
1025 2 gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (click_gesture), GDK_BUTTON_PRIMARY);
1026 2 g_signal_connect_swapped (click_gesture, "pressed", G_CALLBACK (on_click_gesture_pressed), self);
1027 2 gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (click_gesture));
1028
1029 2 gtk_widget_set_overflow (GTK_WIDGET (self), GTK_OVERFLOW_HIDDEN);
1030 2 gtk_widget_set_focusable (GTK_WIDGET (self), TRUE);
1031 2 }
1032
1033 /**
1034 * shumate_map_new:
1035 *
1036 * Creates an instance of #ShumateMap.
1037 *
1038 * Returns: a new #ShumateMap ready to be used as a #GtkWidget.
1039 */
1040 ShumateMap *
1041 2 shumate_map_new (void)
1042 {
1043 2 return g_object_new (SHUMATE_TYPE_MAP, NULL);
1044 }
1045
1046
1047 ShumateMap *
1048 ✗ shumate_map_new_simple (void)
1049 {
1050 ✗ ShumateMap *view = g_object_new (SHUMATE_TYPE_MAP, NULL);
1051 ✗ g_autoptr(ShumateMapSourceRegistry) registry = NULL;
1052 ✗ ShumateMapSource *source;
1053 ✗ ShumateMapLayer *map_layer;
1054 ✗ ShumateViewport *viewport;
1055
1056 ✗ viewport = shumate_map_get_viewport (view);
1057 ✗ registry = shumate_map_source_registry_new_with_defaults ();
1058 ✗ source = shumate_map_source_registry_get_by_id (registry, SHUMATE_MAP_SOURCE_OSM_MAPNIK);
1059 ✗ shumate_viewport_set_reference_map_source (viewport, source);
1060 ✗ map_layer = shumate_map_layer_new (source, viewport);
1061 ✗ shumate_map_add_layer (view, SHUMATE_LAYER (map_layer));
1062
1063 ✗ return view;
1064 }
1065
1066 /**
1067 * shumate_map_get_viewport:
1068 * @self: a #ShumateMap
1069 *
1070 * Get the #ShumateViewport used by this view.
1071 *
1072 * Returns: (transfer none): the #ShumateViewport
1073 */
1074 ShumateViewport *
1075 2 shumate_map_get_viewport (ShumateMap *self)
1076 {
1077
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
2 g_return_val_if_fail (SHUMATE_IS_MAP (self), NULL);
1078
1079 2 return self->viewport;
1080 }
1081
1082 /**
1083 * shumate_map_center_on:
1084 * @self: a #ShumateMap
1085 * @latitude: the longitude to center the map at
1086 * @longitude: the longitude to center the map at
1087 *
1088 * Centers the map on these coordinates.
1089 */
1090 void
1091 ✗ shumate_map_center_on (ShumateMap *self,
1092 double latitude,
1093 double longitude)
1094 {
1095 ✗ g_return_if_fail (SHUMATE_IS_MAP (self));
1096
1097 ✗ shumate_location_set_location (SHUMATE_LOCATION (self->viewport), latitude, longitude);
1098 }
1099
1100 /**
1101 * shumate_map_stop_go_to:
1102 * @self: a #ShumateMap
1103 *
1104 * Stop the go to animation. The view will stay where it was when the
1105 * animation was stopped.
1106 */
1107 void
1108 ✗ shumate_map_stop_go_to (ShumateMap *self)
1109 {
1110 ✗ g_return_if_fail (SHUMATE_IS_MAP (self));
1111
1112 ✗ if (self->goto_context == NULL)
1113 return;
1114
1115 ✗ gtk_widget_remove_tick_callback (GTK_WIDGET (self), self->goto_context->tick_id);
1116 ✗ g_clear_pointer (&self->goto_context, g_free);
1117
1118 ✗ g_signal_emit (self, signals[ANIMATION_COMPLETED], go_to_quark, NULL);
1119 }
1120
1121
1122 /**
1123 * shumate_map_go_to:
1124 * @self: a #ShumateMap
1125 * @latitude: the longitude to center the map at
1126 * @longitude: the longitude to center the map at
1127 *
1128 * Move from the current position to these coordinates. All tiles in the
1129 * intermediate view WILL be loaded!
1130 */
1131 void
1132 ✗ shumate_map_go_to (ShumateMap *self,
1133 double latitude,
1134 double longitude)
1135 {
1136 ✗ double zoom_level;
1137
1138 ✗ g_return_if_fail (SHUMATE_IS_MAP (self));
1139 ✗ g_return_if_fail (latitude >= SHUMATE_MIN_LATITUDE && latitude <= SHUMATE_MAX_LATITUDE);
1140 ✗ g_return_if_fail (longitude >= SHUMATE_MIN_LONGITUDE && longitude <= SHUMATE_MAX_LONGITUDE);
1141
1142 ✗ zoom_level = shumate_viewport_get_zoom_level (self->viewport);
1143
1144 ✗ shumate_map_go_to_full (self, latitude, longitude, zoom_level);
1145 }
1146
1147
1148 /**
1149 * shumate_map_go_to_full:
1150 * @self: a #ShumateMap
1151 * @latitude: the longitude to center the map at
1152 * @longitude: the longitude to center the map at
1153 * @zoom_level: the zoom level to end at
1154 *
1155 * Move from the current position to these coordinates and zoom to the given
1156 * zoom level. All tiles in the intermediate view WILL be loaded!
1157 */
1158 void
1159 ✗ shumate_map_go_to_full (ShumateMap *self,
1160 double latitude,
1161 double longitude,
1162 double zoom_level)
1163 {
1164 ✗ guint duration;
1165
1166 ✗ g_return_if_fail (SHUMATE_IS_MAP (self));
1167 ✗ g_return_if_fail (latitude >= SHUMATE_MIN_LATITUDE && latitude <= SHUMATE_MAX_LATITUDE);
1168 ✗ g_return_if_fail (longitude >= SHUMATE_MIN_LONGITUDE && longitude <= SHUMATE_MAX_LONGITUDE);
1169
1170 ✗ duration = self->go_to_duration;
1171 ✗ if (duration == 0) /* calculate duration from zoom level */
1172 ✗ duration = 500 * zoom_level / 2.0;
1173
1174 ✗ shumate_map_go_to_full_with_duration (self, latitude, longitude, zoom_level, duration);
1175 }
1176
1177
1178 /**
1179 * shumate_map_go_to_full_with_duration:
1180 * @self: a #ShumateMap
1181 * @latitude: the longitude to center the map at
1182 * @longitude: the longitude to center the map at
1183 * @zoom_level: the zoom level to end at
1184 * @duration_ms: animation duration in milliseconds
1185 *
1186 * Move from the current position to these coordinates and zoom to the given
1187 * zoom level. The given duration is used instead of the map's default [property@Map:go-to-duration].
1188 * All tiles in the intermediate view WILL be loaded!
1189 */
1190 void
1191 ✗ shumate_map_go_to_full_with_duration (ShumateMap *self,
1192 double latitude,
1193 double longitude,
1194 double zoom_level,
1195 guint duration_ms)
1196 {
1197 ✗ double min_zoom, max_zoom;
1198 ✗ GoToContext *ctx;
1199 ✗ gboolean enable_animations;
1200
1201 ✗ g_return_if_fail (SHUMATE_IS_MAP (self));
1202 ✗ g_return_if_fail (latitude >= SHUMATE_MIN_LATITUDE && latitude <= SHUMATE_MAX_LATITUDE);
1203 ✗ g_return_if_fail (longitude >= SHUMATE_MIN_LONGITUDE && longitude <= SHUMATE_MAX_LONGITUDE);
1204
1205 ✗ g_object_get (gtk_widget_get_settings (GTK_WIDGET (self)),
1206 "gtk-enable-animations", &enable_animations,
1207 NULL);
1208
1209 ✗ if (!enable_animations || duration_ms == 0)
1210 {
1211 ✗ shumate_map_center_on (self, latitude, longitude);
1212 ✗ shumate_viewport_set_zoom_level (self->viewport, zoom_level);
1213 ✗ return;
1214 }
1215
1216 ✗ shumate_map_stop_go_to (self);
1217
1218 ✗ min_zoom = shumate_viewport_get_min_zoom_level (self->viewport);
1219 ✗ max_zoom = shumate_viewport_get_max_zoom_level (self->viewport);
1220
1221 ✗ ctx = g_new (GoToContext, 1);
1222 ✗ ctx->start_us = g_get_monotonic_time ();
1223 ✗ ctx->duration_us = ms_to_us (duration_ms);
1224 ✗ ctx->from_latitude = shumate_location_get_latitude (SHUMATE_LOCATION (self->viewport));
1225 ✗ ctx->from_longitude = shumate_location_get_longitude (SHUMATE_LOCATION (self->viewport));
1226 ✗ ctx->from_zoom = CLAMP (shumate_viewport_get_zoom_level (self->viewport), min_zoom, max_zoom);
1227 ✗ ctx->to_latitude = latitude;
1228 ✗ ctx->to_longitude = longitude;
1229 ✗ ctx->to_zoom = CLAMP (zoom_level, min_zoom, max_zoom);
1230 ✗ ctx->zoom_animation = FALSE;
1231
1232 ✗ self->goto_context = ctx;
1233
1234 ✗ ctx->tick_id = gtk_widget_add_tick_callback (GTK_WIDGET (self), go_to_tick_cb, NULL, NULL);
1235 }
1236
1237
1238 /**
1239 * shumate_map_get_go_to_duration:
1240 * @self: a #ShumateMap
1241 *
1242 * Get the 'go-to-duration' property.
1243 *
1244 * Returns: the animation duration when calling [method@Map.go_to],
1245 * in milliseconds.
1246 */
1247 guint
1248 ✗ shumate_map_get_go_to_duration (ShumateMap *self)
1249 {
1250 ✗ g_return_val_if_fail (SHUMATE_IS_MAP (self), 0);
1251
1252 ✗ return self->go_to_duration;
1253 }
1254
1255 /**
1256 * shumate_map_set_go_to_duration:
1257 * @self: a #ShumateMap
1258 * @duration: the animation duration, in milliseconds
1259 *
1260 * Set the duration of the transition of [method@Map.go_to].
1261 */
1262 void
1263 ✗ shumate_map_set_go_to_duration (ShumateMap *self,
1264 guint duration)
1265 {
1266 ✗ g_return_if_fail (SHUMATE_IS_MAP (self));
1267
1268 ✗ if (self->go_to_duration == duration)
1269 return;
1270
1271 ✗ self->go_to_duration = duration;
1272 ✗ g_object_notify_by_pspec (G_OBJECT (self), obj_properties[PROP_GO_TO_DURATION]);
1273 }
1274
1275 /**
1276 * shumate_map_add_layer:
1277 * @self: a #ShumateMap
1278 * @layer: a #ShumateLayer
1279 *
1280 * Adds a new layer to the view
1281 */
1282 void
1283 2 shumate_map_add_layer (ShumateMap *self,
1284 ShumateLayer *layer)
1285 {
1286
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
2 g_return_if_fail (SHUMATE_IS_MAP (self));
1287
1/2
✓ Branch 6 → 7 taken 2 times.
✗ Branch 6 → 8 not taken.
2 g_return_if_fail (SHUMATE_IS_LAYER (layer));
1288
1289 2 gtk_widget_insert_before (GTK_WIDGET (layer), GTK_WIDGET (self), NULL);
1290 }
1291
1292
1293 /**
1294 * shumate_map_insert_layer_behind:
1295 * @self: a #ShumateMap
1296 * @layer: a #ShumateLayer
1297 * @next_sibling: (nullable): a #ShumateLayer that is a child of @self, or %NULL
1298 *
1299 * Adds @layer to @self behind @next_sibling or, if @next_sibling is %NULL, at
1300 * the top of the layer list.
1301 */
1302 void
1303 2 shumate_map_insert_layer_behind (ShumateMap *self,
1304 ShumateLayer *layer,
1305 ShumateLayer *next_sibling)
1306 {
1307
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
2 g_return_if_fail (SHUMATE_IS_MAP (self));
1308
1/2
✓ Branch 6 → 7 taken 2 times.
✗ Branch 6 → 8 not taken.
2 g_return_if_fail (SHUMATE_IS_LAYER (layer));
1309
3/4
✓ Branch 7 → 9 taken 1 time.
✓ Branch 7 → 11 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✗ Branch 10 → 12 not taken.
2 g_return_if_fail (next_sibling == NULL || SHUMATE_IS_LAYER (next_sibling));
1310
3/4
✓ Branch 11 → 13 taken 1 time.
✓ Branch 11 → 15 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 16 not taken.
2 g_return_if_fail (next_sibling == NULL || gtk_widget_get_parent (GTK_WIDGET (next_sibling)) == GTK_WIDGET (self));
1311
1312 2 gtk_widget_insert_before (GTK_WIDGET (layer), GTK_WIDGET (self), GTK_WIDGET (next_sibling));
1313 }
1314
1315
1316 /**
1317 * shumate_map_insert_layer_above:
1318 * @self: a #ShumateMap
1319 * @layer: a #ShumateLayer
1320 * @next_sibling: (nullable): a #ShumateLayer that is a child of @self, or %NULL
1321 *
1322 * Adds @layer to @self above @next_sibling or, if @next_sibling is %NULL, at
1323 * the bottom of the layer list.
1324 */
1325 void
1326 2 shumate_map_insert_layer_above (ShumateMap *self,
1327 ShumateLayer *layer,
1328 ShumateLayer *next_sibling)
1329 {
1330
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
2 g_return_if_fail (SHUMATE_IS_MAP (self));
1331
1/2
✓ Branch 6 → 7 taken 2 times.
✗ Branch 6 → 8 not taken.
2 g_return_if_fail (SHUMATE_IS_LAYER (layer));
1332
3/4
✓ Branch 7 → 9 taken 1 time.
✓ Branch 7 → 11 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✗ Branch 10 → 12 not taken.
2 g_return_if_fail (next_sibling == NULL || SHUMATE_IS_LAYER (next_sibling));
1333
3/4
✓ Branch 11 → 13 taken 1 time.
✓ Branch 11 → 15 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 16 not taken.
2 g_return_if_fail (next_sibling == NULL || gtk_widget_get_parent (GTK_WIDGET (next_sibling)) == GTK_WIDGET (self));
1334
1335 2 gtk_widget_insert_after (GTK_WIDGET (layer), GTK_WIDGET (self), GTK_WIDGET (next_sibling));
1336 }
1337
1338
1339 /**
1340 * shumate_map_remove_layer:
1341 * @self: a #ShumateMap
1342 * @layer: a #ShumateLayer
1343 *
1344 * Removes the given layer from the view
1345 */
1346 void
1347 4 shumate_map_remove_layer (ShumateMap *self,
1348 ShumateLayer *layer)
1349 {
1350
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 4 times.
4 g_return_if_fail (SHUMATE_IS_MAP (self));
1351
1/2
✓ Branch 6 → 7 taken 4 times.
✗ Branch 6 → 9 not taken.
4 g_return_if_fail (SHUMATE_IS_LAYER (layer));
1352
1353
1/2
✗ Branch 8 → 10 not taken.
✓ Branch 8 → 12 taken 4 times.
4 if (gtk_widget_get_parent (GTK_WIDGET (layer)) != GTK_WIDGET (self))
1354 {
1355 ✗ g_critical ("The given ShumateLayer isn't a child of the view");
1356 ✗ return;
1357 }
1358
1359 4 gtk_widget_unparent (GTK_WIDGET (layer));
1360 }
1361
1362 /**
1363 * shumate_map_set_map_source:
1364 * @self: a #ShumateMap
1365 * @map_source: a #ShumateMapSource
1366 *
1367 * Changes the currently used map source. #g_object_unref() will be called on
1368 * the previous one.
1369 *
1370 * As a side effect, changing the primary map source will also clear all
1371 * secondary map sources.
1372 */
1373 void
1374 ✗ shumate_map_set_map_source (ShumateMap *self,
1375 ShumateMapSource *source)
1376 {
1377 ✗ ShumateMapSource *ref_map_source;
1378
1379 ✗ g_return_if_fail (SHUMATE_IS_MAP (self));
1380 ✗ g_return_if_fail (SHUMATE_IS_MAP_SOURCE (source));
1381
1382 ✗ ref_map_source = shumate_viewport_get_reference_map_source (self->viewport);
1383 ✗ if (ref_map_source == source)
1384 return;
1385
1386 ✗ shumate_viewport_set_reference_map_source (self->viewport, source);
1387 }
1388
1389 /**
1390 * shumate_map_set_zoom_on_double_click:
1391 * @self: a #ShumateMap
1392 * @value: a #gboolean
1393 *
1394 * Should the view zoom in and recenter when the user double click on the map.
1395 */
1396 void
1397 2 shumate_map_set_zoom_on_double_click (ShumateMap *self,
1398 gboolean value)
1399 {
1400
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
2 g_return_if_fail (SHUMATE_IS_MAP (self));
1401
1402 2 self->zoom_on_double_click = value;
1403 2 g_object_notify_by_pspec (G_OBJECT (self), obj_properties[PROP_ZOOM_ON_DOUBLE_CLICK]);
1404 }
1405
1406
1407 /**
1408 * shumate_map_set_animate_zoom:
1409 * @self: a #ShumateMap
1410 * @value: a #gboolean
1411 *
1412 * Should the view animate zoom level changes.
1413 */
1414 void
1415 ✗ shumate_map_set_animate_zoom (ShumateMap *self,
1416 gboolean value)
1417 {
1418 ✗ g_return_if_fail (SHUMATE_IS_MAP (self));
1419
1420 ✗ self->animate_zoom = value;
1421 ✗ g_object_notify_by_pspec (G_OBJECT (self), obj_properties[PROP_ANIMATE_ZOOM]);
1422 }
1423
1424 /**
1425 * shumate_map_get_zoom_on_double_click:
1426 * @self: a #ShumateMap
1427 *
1428 * Checks whether the view zooms on double click.
1429 *
1430 * Returns: TRUE if the view zooms on double click, FALSE otherwise.
1431 */
1432 gboolean
1433 ✗ shumate_map_get_zoom_on_double_click (ShumateMap *self)
1434 {
1435 ✗ g_return_val_if_fail (SHUMATE_IS_MAP (self), FALSE);
1436
1437 ✗ return self->zoom_on_double_click;
1438 }
1439
1440
1441 /**
1442 * shumate_map_get_animate_zoom:
1443 * @self: a #ShumateMap
1444 *
1445 * Checks whether the view animates zoom level changes.
1446 *
1447 * Returns: TRUE if the view animates zooms, FALSE otherwise.
1448 */
1449 gboolean
1450 ✗ shumate_map_get_animate_zoom (ShumateMap *self)
1451 {
1452 ✗ g_return_val_if_fail (SHUMATE_IS_MAP (self), FALSE);
1453
1454 ✗ return self->animate_zoom;
1455 }
1456
1457 /**
1458 * shumate_map_get_state:
1459 * @self: a #ShumateMap
1460 *
1461 * Gets the view's state.
1462 *
1463 * Returns: the state.
1464 */
1465 ShumateState
1466 ✗ shumate_map_get_state (ShumateMap *self)
1467 {
1468 ✗ g_return_val_if_fail (SHUMATE_IS_MAP (self), SHUMATE_STATE_NONE);
1469
1470 ✗ return self->state;
1471 }
1472
1473 static void
1474 ✗ zoom (ShumateMap *self,
1475 gboolean zoom_out)
1476 {
1477 ✗ double amount = (zoom_out ? -.2 : .2);
1478
1479 /* If there is an ongoing animation, add to it rather than starting a new animation from the current position */
1480 ✗ if (self->goto_context != NULL && self->goto_context->zoom_animation)
1481 {
1482 ✗ shumate_map_go_to_full_with_duration (self,
1483 self->goto_context->to_latitude,
1484 self->goto_context->to_longitude,
1485 ✗ self->goto_context->to_zoom + amount,
1486 ZOOM_ANIMATION_MS);
1487 }
1488 else
1489 {
1490 ✗ double zoom_level = shumate_viewport_get_zoom_level (self->viewport);
1491 ✗ shumate_map_go_to_full_with_duration (self,
1492 ✗ shumate_location_get_latitude (SHUMATE_LOCATION (self->viewport)),
1493 ✗ shumate_location_get_longitude (SHUMATE_LOCATION (self->viewport)),
1494 ✗ roundf ((zoom_level + amount) * 5) / 5,
1495 ✗ self->animate_zoom ? ZOOM_ANIMATION_MS : 0);
1496 }
1497
1498 ✗ if (self->goto_context != NULL)
1499 ✗ self->goto_context->zoom_animation = TRUE;
1500 ✗ }
1501
1502 /**
1503 * shumate_map_zoom_in:
1504 * @self: a [class@Map]
1505 *
1506 * Zooms the map in. If [property@Map:animate-zoom] is `TRUE`, the change will be animated.
1507 */
1508 void
1509 ✗ shumate_map_zoom_in (ShumateMap *self)
1510 {
1511 ✗ g_return_if_fail (SHUMATE_IS_MAP (self));
1512 ✗ zoom (self, FALSE);
1513 }
1514
1515 /**
1516 * shumate_map_zoom_out:
1517 * @self: a [class@Map]
1518 *
1519 * Zooms the map out. If [property@Map:animate-zoom] is `TRUE`, the change will be animated.
1520 */
1521 void
1522 ✗ shumate_map_zoom_out (ShumateMap *self)
1523 {
1524 ✗ g_return_if_fail (SHUMATE_IS_MAP (self));
1525 ✗ zoom (self, TRUE);
1526 }
1527