shumate-map.c 41.4 KB
Newer Older
1
2
3
/*
 * Copyright (C) 2008-2009 Pierre-Luc Beaudoin <pierre-luc@pierlux.com>
 * Copyright (C) 2010-2013 Jiri Techet <techet@gmail.com>
4
 * Copyright (C) 2012,2020 Collabora Ltd. (https://www.collabora.com/)
Marcus Lundblad's avatar
Marcus Lundblad committed
5
 * Copyright (C) 2019 Marcus Lundblad <ml@update.uu.se>
6
 * Copyright (C) 2020 Corentin Noël <corentin.noel@collabora.com>
Marcus Lundblad's avatar
Marcus Lundblad committed
7
 *
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

/**
25
 * ShumateMap:
26
 *
27
28
29
30
31
32
 * The Map widget is a [class@Gtk.Widget] that show and allows interaction with
 * the user.
 *
 * This is the base widget and doesn't have advanced features. You can check the
 * [class@Shumate.SimpleMap] for a ready-to-use widget.
 *
James Westman's avatar
James Westman committed
33
 * By default, a [class@Shumate.Viewport] is created and can be accessed with
34
35
36
37
38
39
 * [method@Shumate.Map.get_viewport].
 *
 * Unless created with [ctor@Shumate.Map.new_simple], the widget doesn't hold any
 * layer and won't show anything. A [class@Shumate.Layer] can be added or removed
 * using the [method@Shumate.Map.add_layer] or [method@Shumate.Map.remove_layer]
 * methods.
40
41
 */

42
#include "shumate-map.h"
43

Marcus Lundblad's avatar
Marcus Lundblad committed
44
45
#include "shumate.h"
#include "shumate-enum-types.h"
46
#include "shumate-kinetic-scrolling-private.h"
Marcus Lundblad's avatar
Marcus Lundblad committed
47
#include "shumate-marshal.h"
48
#include "shumate-map-layer.h"
Marcus Lundblad's avatar
Marcus Lundblad committed
49
#include "shumate-map-source.h"
50
#include "shumate-map-source-registry.h"
Marcus Lundblad's avatar
Marcus Lundblad committed
51
52
#include "shumate-tile.h"
#include "shumate-license.h"
53
#include "shumate-location.h"
54
#include "shumate-viewport-private.h"
55
56
57

#include <glib.h>
#include <glib-object.h>
58
#include <gtk/gtk.h>
59
60
#include <math.h>

61
#define DECELERATION_FRICTION 4.0
James Westman's avatar
James Westman committed
62
#define ZOOM_ANIMATION_MS 200
63

64
65
66
67
68
69
70
71
72
enum
{
  /* normal signals */
  ANIMATION_COMPLETED,
  LAST_SIGNAL
};

enum
{
73
  PROP_ZOOM_ON_DOUBLE_CLICK = 1,
74
75
  PROP_ANIMATE_ZOOM,
  PROP_STATE,
76
  PROP_GO_TO_DURATION,
77
  PROP_VIEWPORT,
78
  N_PROPERTIES
79
80
81
82
};

static guint signals[LAST_SIGNAL] = { 0, };

83
static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, };
84
static GQuark go_to_quark;
85

86
87
88
/* Between state values for go_to */
typedef struct
{
89
90
  int64_t duration_us;
  int64_t start_us;
91
92
  double to_latitude;
  double to_longitude;
James Westman's avatar
James Westman committed
93
  double to_zoom;
94
95
  double from_latitude;
  double from_longitude;
James Westman's avatar
James Westman committed
96
  double from_zoom;
97
  guint tick_id;
James Westman's avatar
James Westman committed
98
  gboolean zoom_animation : 1;
99
100
} GoToContext;

101
102
103
typedef struct
{
  ShumateKineticScrolling *kinetic_scrolling;
104
  ShumateMap *map;
105
106
107
108
109
  double start_lat;
  double start_lon;
  int64_t last_deceleration_time_us;
  graphene_vec2_t direction;
} KineticScrollData;
110

James Westman's avatar
James Westman committed
111
struct _ShumateMap
112
{
James Westman's avatar
James Westman committed
113
114
  GtkWidget parent_instance;

115
116
  ShumateViewport *viewport;

117
118
119
  gboolean zoom_on_double_click;
  gboolean animate_zoom;

Marcus Lundblad's avatar
Marcus Lundblad committed
120
  ShumateState state; /* View's global state */
121
122
123

  GoToContext *goto_context;

124
125
  guint deceleration_tick_id;

126
  guint zoom_timeout;
Marcus Lundblad's avatar
Marcus Lundblad committed
127

128
  guint go_to_duration;
129

130
131
  double current_x;
  double current_y;
132

133
  double zoom_level_begin;
James Westman's avatar
James Westman committed
134
  double rotate_begin;
135

James Westman's avatar
James Westman committed
136
137
138
139
  double gesture_begin_lat;
  double gesture_begin_lon;
  double drag_begin_x;
  double drag_begin_y;
James Westman's avatar
James Westman committed
140
};
141

James Westman's avatar
James Westman committed
142
G_DEFINE_TYPE (ShumateMap, shumate_map, GTK_TYPE_WIDGET);
143

James Westman's avatar
James Westman committed
144
145
146
147
148
149
150
static double
positive_mod (double i, double n)
{
  return fmod (fmod (i, n) + n, n);
}

static void
James Westman's avatar
James Westman committed
151
152
153
154
155
156
move_location_to_coords_calc (ShumateMap      *self,
                              double          *lat,
                              double          *lon,
                              double           x,
                              double           y,
                              ShumateViewport *viewport)
James Westman's avatar
James Westman committed
157
{
James Westman's avatar
James Westman committed
158
159
  ShumateMapSource *map_source = shumate_viewport_get_reference_map_source (viewport);
  double zoom_level = shumate_viewport_get_zoom_level (viewport);
James Westman's avatar
James Westman committed
160
161
162
163
164
165
166
167
168
169
170
  double tile_size, map_width, map_height;
  double map_x, map_y;
  double target_lat, target_lon;
  double target_map_x, target_map_y;
  double current_lat, current_lon;
  double current_map_x, current_map_y;
  double new_map_x, new_map_y;

  if (map_source == NULL)
    return;

James Westman's avatar
James Westman committed
171
  tile_size = shumate_map_source_get_tile_size_at_zoom (map_source, zoom_level);
James Westman's avatar
James Westman committed
172
173
174
  map_width = tile_size * shumate_map_source_get_column_count (map_source, zoom_level);
  map_height = tile_size * shumate_map_source_get_row_count (map_source, zoom_level);

James Westman's avatar
James Westman committed
175
176
  map_x = shumate_map_source_get_x (map_source, zoom_level, *lon);
  map_y = shumate_map_source_get_y (map_source, zoom_level, *lat);
James Westman's avatar
James Westman committed
177

James Westman's avatar
James Westman committed
178
179
  current_lat = shumate_location_get_latitude (SHUMATE_LOCATION (viewport));
  current_lon = shumate_location_get_longitude (SHUMATE_LOCATION (viewport));
James Westman's avatar
James Westman committed
180
181
182
  current_map_x = shumate_map_source_get_x (map_source, zoom_level, current_lon);
  current_map_y = shumate_map_source_get_y (map_source, zoom_level, current_lat);

James Westman's avatar
James Westman committed
183
  shumate_viewport_widget_coords_to_location (viewport, GTK_WIDGET (self), x, y, &target_lat, &target_lon);
James Westman's avatar
James Westman committed
184
185
186
187
188
189
  target_map_x = shumate_map_source_get_x (map_source, zoom_level, target_lon);
  target_map_y = shumate_map_source_get_y (map_source, zoom_level, target_lat);

  new_map_x = positive_mod (current_map_x - (target_map_x - map_x), map_width);
  new_map_y = positive_mod (current_map_y - (target_map_y - map_y), map_height);

James Westman's avatar
James Westman committed
190
191
192
193
194
195
196
197
198
199
200
  *lat = shumate_map_source_get_latitude (map_source, zoom_level, new_map_y);
  *lon = shumate_map_source_get_longitude (map_source, zoom_level, new_map_x);
}

static void
move_location_to_coords (ShumateMap *self,
                         double lat,
                         double lon,
                         double x,
                         double y)
{
James Westman's avatar
James Westman committed
201
202
  move_location_to_coords_calc (self, &lat, &lon, x, y, self->viewport);
  shumate_location_set_location (SHUMATE_LOCATION (self->viewport), lat, lon);
James Westman's avatar
James Westman committed
203
204
}

205
static void
206
207
208
209
210
move_viewport_from_pixel_offset (ShumateMap *self,
                                 double      latitude,
                                 double      longitude,
                                 double      offset_x,
                                 double      offset_y)
211
212
213
214
215
{
  ShumateMapSource *map_source;
  double x, y;
  double lat, lon;

216
  g_assert (SHUMATE_IS_MAP (self));
217

James Westman's avatar
James Westman committed
218
  map_source = shumate_viewport_get_reference_map_source (self->viewport);
219
220
221
  if (!map_source)
    return;

James Westman's avatar
James Westman committed
222
  shumate_viewport_location_to_widget_coords (self->viewport, GTK_WIDGET (self), latitude, longitude, &x, &y);
223

James Westman's avatar
James Westman committed
224
225
  x -= offset_x;
  y -= offset_y;
226

James Westman's avatar
James Westman committed
227
  shumate_viewport_widget_coords_to_location (self->viewport, GTK_WIDGET (self), x, y, &lat, &lon);
228

James Westman's avatar
James Westman committed
229
230
  lat = fmod (lat + 90, 180) - 90;
  lon = fmod (lon + 180, 360) - 180;
231

James Westman's avatar
James Westman committed
232
  shumate_location_set_location (SHUMATE_LOCATION (self->viewport), lat, lon);
233
234
}

235
static void
236
cancel_deceleration (ShumateMap *self)
237
{
James Westman's avatar
James Westman committed
238
  if (self->deceleration_tick_id > 0)
239
    {
James Westman's avatar
James Westman committed
240
241
      gtk_widget_remove_tick_callback (GTK_WIDGET (self), self->deceleration_tick_id);
      self->deceleration_tick_id = 0;
242
243
244
245
246
247
248
249
250
    }
}

static gboolean
view_deceleration_tick_cb (GtkWidget     *widget,
                           GdkFrameClock *frame_clock,
                           gpointer       user_data)
{
  KineticScrollData *data = user_data;
251
  ShumateMap *map = data->map;
252
253
254
255
  int64_t current_time_us;
  double elapsed_us;
  double position;

256
  g_assert (SHUMATE_IS_MAP (map));
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277

  current_time_us = gdk_frame_clock_get_frame_time (frame_clock);
  elapsed_us = current_time_us - data->last_deceleration_time_us;

  /* The frame clock can sometimes fire immediately after adding a tick callback,
   * in which case no time has passed, making it impossible to calculate the
   * kinetic factor. If this is the case, wait for the next tick.
   */
  if (G_APPROX_VALUE (elapsed_us, 0.0, FLT_EPSILON))
    return G_SOURCE_CONTINUE;

  data->last_deceleration_time_us = current_time_us;

  if (data->kinetic_scrolling &&
      shumate_kinetic_scrolling_tick (data->kinetic_scrolling, elapsed_us, &position))
    {
      graphene_vec2_t new_positions;

      graphene_vec2_init (&new_positions, position, position);
      graphene_vec2_multiply (&new_positions, &data->direction, &new_positions);

278
      move_viewport_from_pixel_offset (map,
279
280
281
282
283
284
285
286
287
288
289
290
                                       data->start_lat,
                                       data->start_lon,
                                       graphene_vec2_get_x (&new_positions),
                                       graphene_vec2_get_y (&new_positions));
    }
  else
    {
      g_clear_pointer (&data->kinetic_scrolling, shumate_kinetic_scrolling_free);
    }

  if (!data->kinetic_scrolling)
    {
291
      cancel_deceleration (map);
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
      return G_SOURCE_REMOVE;
    }

  return G_SOURCE_CONTINUE;
}


static void
kinetic_scroll_data_free (KineticScrollData *data)
{
  if (data == NULL)
    return;

  g_clear_pointer (&data->kinetic_scrolling, shumate_kinetic_scrolling_free);
  g_free (data);
}

static void
310
311
312
start_deceleration (ShumateMap *self,
                    double      h_velocity,
                    double      v_velocity)
313
314
315
316
317
{
  GdkFrameClock *frame_clock;
  KineticScrollData *data;
  graphene_vec2_t velocity;

James Westman's avatar
James Westman committed
318
  g_assert (self->deceleration_tick_id == 0);
319
320
321
322
323
324

  frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self));

  graphene_vec2_init (&velocity, h_velocity, v_velocity);

  data = g_new0 (KineticScrollData, 1);
325
  data->map = self;
326
  data->last_deceleration_time_us = gdk_frame_clock_get_frame_time (frame_clock);
James Westman's avatar
James Westman committed
327
328
  data->start_lat = shumate_location_get_latitude (SHUMATE_LOCATION (self->viewport));
  data->start_lon = shumate_location_get_longitude (SHUMATE_LOCATION (self->viewport));
329
330
331
332
333
  graphene_vec2_normalize (&velocity, &data->direction);
  data->kinetic_scrolling =
    shumate_kinetic_scrolling_new (DECELERATION_FRICTION,
                                   graphene_vec2_length (&velocity));

James Westman's avatar
James Westman committed
334
  self->deceleration_tick_id =
335
336
337
338
339
340
    gtk_widget_add_tick_callback (GTK_WIDGET (self),
                                  view_deceleration_tick_cb,
                                  data,
                                  (GDestroyNotify) kinetic_scroll_data_free);
}

341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
static inline double
ease_in_out_quad (double p)
{
  p = 2.0 * p;
  if (p < 1.0)
    return 0.5 * p * p;

  p -= 1.0;
  return -0.5 * (p * (p - 2) - 1);
}

static inline int64_t
ms_to_us (int64_t ms)
{
  return ms * 1000;
}

static gboolean
go_to_tick_cb (GtkWidget     *widget,
               GdkFrameClock *frame_clock,
               gpointer       user_data)
{
James Westman's avatar
James Westman committed
363
  ShumateMap *self = SHUMATE_MAP (widget);
James Westman's avatar
James Westman committed
364
  GoToContext *ctx;
365
366
367
  int64_t now_us;
  double latitude, longitude;
  double progress;
James Westman's avatar
James Westman committed
368
369
370
371
  double current_zoom;

  g_assert (SHUMATE_IS_MAP (widget));
  g_assert (user_data == NULL);
372

James Westman's avatar
James Westman committed
373
  ctx = self->goto_context;
James Westman's avatar
James Westman committed
374
375

  g_assert (ctx != NULL);
376
377
378
379
380
381
  g_assert (ctx->duration_us >= 0);

  now_us = g_get_monotonic_time ();

  if (now_us >= ctx->start_us + ctx->duration_us)
    {
James Westman's avatar
James Westman committed
382
      shumate_location_set_location (SHUMATE_LOCATION (self->viewport),
383
384
                                     ctx->to_latitude,
                                     ctx->to_longitude);
James Westman's avatar
James Westman committed
385
      shumate_viewport_set_zoom_level (self->viewport, ctx->to_zoom);
James Westman's avatar
James Westman committed
386
      shumate_map_stop_go_to (SHUMATE_MAP (widget));
387
388
389
390
391
392
393
      return G_SOURCE_REMOVE;
    }

  progress = (now_us - ctx->start_us) / (double) ctx->duration_us;
  g_assert (progress >= 0.0 && progress <= 1.0);

  /* Apply the ease function to the progress itself */
James Westman's avatar
James Westman committed
394
395
  if (!ctx->zoom_animation)
    progress = ease_in_out_quad (progress);
396

James Westman's avatar
James Westman committed
397
398
  /* Interpolate zoom level */
  current_zoom = ctx->from_zoom + (ctx->to_zoom - ctx->from_zoom) * progress;
James Westman's avatar
James Westman committed
399
  shumate_viewport_set_zoom_level (self->viewport, current_zoom);
James Westman's avatar
James Westman committed
400
401
402
403
404
405
406

  /* If we're zooming, we need to adjust for that in the progress, otherwise
   * the animation will speed up at higher zoom levels. */
  if (ctx->to_zoom != ctx->from_zoom)
    progress = (pow (2, -ctx->from_zoom) - pow (2, -current_zoom))
                / (pow (2, -ctx->from_zoom) - pow (2, -ctx->to_zoom));

407
408
409
410
411
412
  /* Since progress already follows the easing curve, a simple LERP is guaranteed
   * to follow it too.
   */
  latitude = ctx->from_latitude + (ctx->to_latitude - ctx->from_latitude) * progress;
  longitude = ctx->from_longitude + (ctx->to_longitude - ctx->from_longitude) * progress;

James Westman's avatar
James Westman committed
413
  shumate_location_set_location (SHUMATE_LOCATION (self->viewport), latitude, longitude);
414
415
416
417

  return G_SOURCE_CONTINUE;
}

418
static void
419
420
421
on_drag_gesture_drag_begin (ShumateMap     *self,
                            double          start_x,
                            double          start_y,
422
423
                            GtkGestureDrag *gesture)
{
424
  g_assert (SHUMATE_IS_MAP (self));
425

426
427
  cancel_deceleration (self);

James Westman's avatar
James Westman committed
428
429
  self->drag_begin_x = start_x;
  self->drag_begin_y = start_y;
James Westman's avatar
James Westman committed
430

James Westman's avatar
James Westman committed
431
  shumate_viewport_widget_coords_to_location (self->viewport, GTK_WIDGET (self),
James Westman's avatar
James Westman committed
432
                                              start_x, start_y,
James Westman's avatar
James Westman committed
433
434
                                              &self->gesture_begin_lat,
                                              &self->gesture_begin_lon);
435
436
437
438
439

  gtk_widget_set_cursor_from_name (GTK_WIDGET (self), "grabbing");
}

static void
440
441
442
on_drag_gesture_drag_update (ShumateMap     *self,
                             double          offset_x,
                             double          offset_y,
443
444
                             GtkGestureDrag *gesture)
{
James Westman's avatar
James Westman committed
445
  move_location_to_coords (self,
James Westman's avatar
James Westman committed
446
447
448
449
                           self->gesture_begin_lat,
                           self->gesture_begin_lon,
                           self->drag_begin_x + offset_x,
                           self->drag_begin_y + offset_y);
450
451
452
}

static void
453
454
455
on_drag_gesture_drag_end (ShumateMap     *self,
                          double          offset_x,
                          double          offset_y,
456
457
                          GtkGestureDrag *gesture)
{
458
  g_assert (SHUMATE_IS_MAP (self));
459

Corentin Noël's avatar
Corentin Noël committed
460
461
  gtk_widget_set_cursor_from_name (GTK_WIDGET (self), "grab");

James Westman's avatar
James Westman committed
462
463
  self->gesture_begin_lon = 0;
  self->gesture_begin_lat = 0;
464
465
}

466
467
468
469
static void
view_swipe_cb (GtkGestureSwipe *swipe_gesture,
               double           velocity_x,
               double           velocity_y,
470
               ShumateMap      *self)
471
472
473
474
{
  start_deceleration (self, velocity_x, velocity_y);
}

475
static void
476
477
set_zoom_level (ShumateMap *self,
                double      zoom_level)
478
{
479
  ShumateMapSource *map_source;
James Westman's avatar
James Westman committed
480
  double lat, lon;
481

James Westman's avatar
James Westman committed
482
  g_object_freeze_notify (G_OBJECT (self->viewport));
483

James Westman's avatar
James Westman committed
484
  map_source = shumate_viewport_get_reference_map_source (self->viewport);
485
  if (map_source)
James Westman's avatar
James Westman committed
486
    shumate_viewport_widget_coords_to_location (self->viewport,
James Westman's avatar
James Westman committed
487
                                                GTK_WIDGET (self),
James Westman's avatar
James Westman committed
488
                                                self->current_x, self->current_y,
James Westman's avatar
James Westman committed
489
                                                &lat, &lon);
490

491
  if (map_source)
James Westman's avatar
James Westman committed
492
    {
James Westman's avatar
James Westman committed
493
      g_autoptr(ShumateViewport) new_viewport = shumate_viewport_copy (self->viewport);
James Westman's avatar
James Westman committed
494
      shumate_viewport_set_zoom_level (new_viewport, zoom_level);
James Westman's avatar
James Westman committed
495
      move_location_to_coords_calc (self, &lat, &lon, self->current_x, self->current_y, new_viewport);
James Westman's avatar
James Westman committed
496
497
498
499
500

      shumate_map_go_to_full_with_duration (self,
                                            lat,
                                            lon,
                                            zoom_level,
James Westman's avatar
James Westman committed
501
                                            self->animate_zoom ? ZOOM_ANIMATION_MS : 0);
James Westman's avatar
James Westman committed
502

James Westman's avatar
James Westman committed
503
504
      if (self->goto_context != NULL)
        self->goto_context->zoom_animation = TRUE;
James Westman's avatar
James Westman committed
505
506
    }
  else
James Westman's avatar
James Westman committed
507
    shumate_viewport_set_zoom_level (self->viewport, zoom_level);
James Westman's avatar
James Westman committed
508

James Westman's avatar
James Westman committed
509
  g_object_thaw_notify (G_OBJECT (self->viewport));
510
511
512
}

static gboolean
513
514
515
on_scroll_controller_scroll (ShumateMap               *self,
                             double                    dx,
                             double                    dy,
516
517
                             GtkEventControllerScroll *controller)
{
James Westman's avatar
James Westman committed
518
  double zoom_level = shumate_viewport_get_zoom_level (self->viewport);
519

James Westman's avatar
James Westman committed
520
521
  if (self->goto_context != NULL && self->goto_context->zoom_animation)
    zoom_level = self->goto_context->to_zoom;
James Westman's avatar
James Westman committed
522

523
524
525
526
527
528
529
  if (dy < 0)
    zoom_level += 0.2;
  else if (dy > 0)
    zoom_level -= 0.2;

  /* snap to the nearest 1/5 of a zoom level */
  set_zoom_level (self, roundf (zoom_level * 5) / 5);
530

531
532
533
  return TRUE;
}

534
static void
535
on_zoom_gesture_begin (ShumateMap       *self,
536
                       GdkEventSequence *seq,
537
                       GtkGestureZoom   *zoom)
538
{
James Westman's avatar
James Westman committed
539
  double zoom_level = shumate_viewport_get_zoom_level (self->viewport);
James Westman's avatar
James Westman committed
540
  double x, y;
541
542
543
544

  gtk_gesture_set_state (GTK_GESTURE (zoom), GTK_EVENT_SEQUENCE_CLAIMED);
  cancel_deceleration (self);

James Westman's avatar
James Westman committed
545
  self->zoom_level_begin = zoom_level;
546

James Westman's avatar
James Westman committed
547
  gtk_gesture_get_bounding_box_center (GTK_GESTURE (zoom), &x, &y);
James Westman's avatar
James Westman committed
548
  shumate_viewport_widget_coords_to_location (self->viewport, GTK_WIDGET (self),
James Westman's avatar
James Westman committed
549
                                              x, y,
James Westman's avatar
James Westman committed
550
551
                                              &self->gesture_begin_lat,
                                              &self->gesture_begin_lon);
552
553
554
}

static void
555
on_zoom_gesture_update (ShumateMap       *self,
556
                        GdkEventSequence *seq,
557
                        GtkGestureZoom   *zoom)
558
559
560
561
562
{
  double x, y;
  double scale = gtk_gesture_zoom_get_scale_delta (zoom);

  gtk_gesture_get_bounding_box_center (GTK_GESTURE (zoom), &x, &y);
James Westman's avatar
James Westman committed
563
564
  shumate_viewport_set_zoom_level (self->viewport, self->zoom_level_begin + log (scale) / G_LN2);
  move_location_to_coords (self, self->gesture_begin_lat, self->gesture_begin_lon, x, y);
James Westman's avatar
James Westman committed
565
}
566

James Westman's avatar
James Westman committed
567
568
569
570
571
static void
on_rotate_gesture_begin (ShumateMap *self,
                         GdkEventSequence *seq,
                         GtkGestureRotate *rotate)
{
James Westman's avatar
James Westman committed
572
  double rotation = shumate_viewport_get_rotation (self->viewport);
573

James Westman's avatar
James Westman committed
574
575
576
  gtk_gesture_set_state (GTK_GESTURE (rotate), GTK_EVENT_SEQUENCE_CLAIMED);
  cancel_deceleration (self);

James Westman's avatar
James Westman committed
577
  self->rotate_begin = rotation;
James Westman's avatar
James Westman committed
578
579
580
581
582
583
584
585
586
587
}

static void
on_rotate_gesture_update (ShumateMap *self,
                          GdkEventSequence *seq,
                          GtkGestureRotate *rotate)
{
  double rotation;
  double x, y;

James Westman's avatar
James Westman committed
588
  rotation = gtk_gesture_rotate_get_angle_delta (rotate) + self->rotate_begin;
589

James Westman's avatar
James Westman committed
590
591
592
593
  /* snap to due north */
  if (fabs (fmod (rotation - 0.25, G_PI * 2)) < 0.5)
    rotation = 0.0;

James Westman's avatar
James Westman committed
594
  shumate_viewport_set_rotation (self->viewport, rotation);
James Westman's avatar
James Westman committed
595
  gtk_gesture_get_bounding_box_center (GTK_GESTURE (rotate), &x, &y);
James Westman's avatar
James Westman committed
596
  move_location_to_coords (self, self->gesture_begin_lat, self->gesture_begin_lon, x, y);
597
598
}

599
600
static void
on_click_gesture_pressed (ShumateMap      *self,
601
602
603
                          int              n_press,
                          double           x,
                          double           y,
604
605
                          GtkGestureClick *click)
{
606
  if (n_press == 2)
607
608
609
610
611
612
613
614
    {
      double zoom_level = shumate_viewport_get_zoom_level (self->viewport);
      self->current_x = x;
      self->current_y = y;
      set_zoom_level (self, zoom_level + 1);
    }
}

615
static void
616
617
618
on_motion_controller_motion (ShumateMap               *self,
                             double                    x,
                             double                    y,
619
620
                             GtkEventControllerMotion *controller)
{
James Westman's avatar
James Westman committed
621
622
  self->current_x = x;
  self->current_y = y;
623
624
}

625
static void
626
627
628
629
shumate_map_get_property (GObject    *object,
                          guint       prop_id,
                          GValue     *value,
                          GParamSpec *pspec)
630
{
James Westman's avatar
James Westman committed
631
  ShumateMap *self = SHUMATE_MAP (object);
632
633
634
635

  switch (prop_id)
    {
    case PROP_ZOOM_ON_DOUBLE_CLICK:
James Westman's avatar
James Westman committed
636
      g_value_set_boolean (value, self->zoom_on_double_click);
637
638
639
      break;

    case PROP_ANIMATE_ZOOM:
James Westman's avatar
James Westman committed
640
      g_value_set_boolean (value, self->animate_zoom);
641
642
643
      break;

    case PROP_STATE:
James Westman's avatar
James Westman committed
644
      g_value_set_enum (value, self->state);
645
646
      break;

647
    case PROP_GO_TO_DURATION:
James Westman's avatar
James Westman committed
648
      g_value_set_uint (value, self->go_to_duration);
649
650
      break;

651
    case PROP_VIEWPORT:
James Westman's avatar
James Westman committed
652
      g_value_set_object (value, self->viewport);
653
654
      break;

655
656
657
658
659
660
661
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}


static void
662
663
664
665
shumate_map_set_property (GObject      *object,
                          guint         prop_id,
                          const GValue *value,
                          GParamSpec   *pspec)
666
{
James Westman's avatar
James Westman committed
667
  ShumateMap *self = SHUMATE_MAP (object);
668
669
670
671

  switch (prop_id)
    {
    case PROP_ZOOM_ON_DOUBLE_CLICK:
James Westman's avatar
James Westman committed
672
      shumate_map_set_zoom_on_double_click (self, g_value_get_boolean (value));
673
674
675
      break;

    case PROP_ANIMATE_ZOOM:
James Westman's avatar
James Westman committed
676
      shumate_map_set_animate_zoom (self, g_value_get_boolean (value));
677
      break;
Marcus Lundblad's avatar
Marcus Lundblad committed
678

679
    case PROP_GO_TO_DURATION:
James Westman's avatar
James Westman committed
680
      shumate_map_set_go_to_duration (self, g_value_get_uint (value));
681
682
683
684
685
686
687
688
689
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}


static void
690
shumate_map_dispose (GObject *object)
691
{
James Westman's avatar
James Westman committed
692
  ShumateMap *self = SHUMATE_MAP (object);
693
  GtkWidget *child;
694

James Westman's avatar
James Westman committed
695
696
  if (self->goto_context != NULL)
    shumate_map_stop_go_to (self);
697

698
699
700
  while ((child = gtk_widget_get_first_child (GTK_WIDGET (object))))
    gtk_widget_unparent (child);

James Westman's avatar
James Westman committed
701
  g_clear_object (&self->viewport);
702

James Westman's avatar
James Westman committed
703
  g_clear_handle_id (&self->zoom_timeout, g_source_remove);
704

705
  G_OBJECT_CLASS (shumate_map_parent_class)->dispose (object);
706
707
708
}

static void
709
shumate_map_class_init (ShumateMapClass *klass)
710
{
711
712
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
James Westman's avatar
James Westman committed
713

714
715
716
  object_class->dispose = shumate_map_dispose;
  object_class->get_property = shumate_map_get_property;
  object_class->set_property = shumate_map_set_property;
717
718

  /**
719
   * ShumateMap:zoom-on-double-click:
720
721
722
   *
   * Should the view zoom in and recenter when the user double click on the map.
   */
723
724
725
726
727
728
  obj_properties[PROP_ZOOM_ON_DOUBLE_CLICK] =
    g_param_spec_boolean ("zoom-on-double-click",
                          "Zoom in on double click",
                          "Zoom in and recenter on double click on the map",
                          TRUE,
                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
729
730

  /**
731
   * ShumateMap:animate-zoom:
732
733
734
   *
   * Animate zoom change when zooming in/out.
   */
735
736
737
738
739
740
  obj_properties[PROP_ANIMATE_ZOOM] =
    g_param_spec_boolean ("animate-zoom",
                          "Animate zoom level change",
                          "Animate zoom change when zooming in/out",
                          TRUE,
                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
741
742

  /**
743
   * ShumateMap:state:
744
745
746
747
   *
   * The view's global state. Useful to inform using if the view is busy loading
   * tiles or not.
   */
748
749
750
751
752
753
754
  obj_properties[PROP_STATE] =
    g_param_spec_enum ("state",
                       "View's state",
                       "View's global state",
                       SHUMATE_TYPE_STATE,
                       SHUMATE_STATE_NONE,
                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
755
756

  /**
757
   * ShumateMap:go-to-duration:
758
   *
759
   * The duration of an animation when going to a location, in milliseconds.
760
761
   * A value of 0 means that the duration is calculated automatically for you.
   *
762
   * Please note that animation of #shumate_map_ensure_visible also
763
   * involves a 'go-to' animation.
764
765
   *
   */
766
767
  obj_properties[PROP_GO_TO_DURATION] =
    g_param_spec_uint ("go-to-duration",
768
769
770
771
                       "Go to animation duration",
                       "The duration of an animation when going to a location",
                       0, G_MAXUINT, 0,
                       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
772

773
774
775
776
777
778
779
780
781
782
783
784
785
  /**
   * ShumateMap:viewport:
   *
   * The viewport, which contains information about the center, rotation, zoom,
   * etc. of the map.
   */
  obj_properties[PROP_VIEWPORT] =
    g_param_spec_object ("viewport",
                         "Viewport",
                         "Viewport",
                         SHUMATE_TYPE_VIEWPORT,
                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

786
787
788
  g_object_class_install_properties (object_class,
                                     N_PROPERTIES,
                                     obj_properties);
789
790

  /**
791
   * ShumateMap::animation-completed:
792
   *
793
   * The #ShumateMap::animation-completed signal is emitted when any animation in the view
794
795
796
797
798
   * ends.  This is a detailed signal.  For example, if you want to be signaled
   * only for go-to animation, you should connect to
   * "animation-completed::go-to". And for zoom, connect to "animation-completed::zoom".
   */
  signals[ANIMATION_COMPLETED] =
Marcus Lundblad's avatar
Marcus Lundblad committed
799
    g_signal_new ("animation-completed",
800
801
802
803
804
805
                  G_OBJECT_CLASS_TYPE (object_class),
                  G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
                  0, NULL, NULL,
                  g_cclosure_marshal_VOID__OBJECT,
                  G_TYPE_NONE,
                  0);
806

807
  gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
808
  gtk_widget_class_set_css_name (widget_class, "map-view");
809
810

  go_to_quark = g_quark_from_static_string ("go-to");
811
812
}

813
static void
814
shumate_map_init (ShumateMap *self)
815
{
816
817
  GtkGesture *drag_gesture;
  GtkEventController *scroll_controller;
818
  GtkEventController *motion_controller;
819
  GtkEventController *key_controller;
820
  GtkGesture *swipe_gesture;
821
  GtkGesture *zoom_gesture;
James Westman's avatar
James Westman committed
822
  GtkGesture *rotate_gesture;
823
  GtkGesture *click_gesture;
Marcus Lundblad's avatar
Marcus Lundblad committed
824

James Westman's avatar
James Westman committed
825
826
827
828
829
830
  self->viewport = shumate_viewport_new ();
  self->zoom_on_double_click = TRUE;
  self->animate_zoom = TRUE;
  self->state = SHUMATE_STATE_NONE;
  self->goto_context = NULL;
  self->go_to_duration = 0;
831

832
  gtk_widget_set_cursor_from_name (GTK_WIDGET (self), "grab");
833

834
  drag_gesture = gtk_gesture_drag_new ();
835
836
837
838
  g_signal_connect_swapped (drag_gesture, "drag-begin", G_CALLBACK (on_drag_gesture_drag_begin), self);
  g_signal_connect_swapped (drag_gesture, "drag-update", G_CALLBACK (on_drag_gesture_drag_update), self);
  g_signal_connect_swapped (drag_gesture, "drag-end", G_CALLBACK (on_drag_gesture_drag_end), self);
  gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (drag_gesture));
839

840
  swipe_gesture = gtk_gesture_swipe_new ();
841
842
  g_signal_connect (swipe_gesture, "swipe", G_CALLBACK (view_swipe_cb), self);
  gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (swipe_gesture));
843

844
  scroll_controller = gtk_event_controller_scroll_new (GTK_EVENT_CONTROLLER_SCROLL_VERTICAL|GTK_EVENT_CONTROLLER_SCROLL_DISCRETE);
845
846
  g_signal_connect_swapped (scroll_controller, "scroll", G_CALLBACK (on_scroll_controller_scroll), self);
  gtk_widget_add_controller (GTK_WIDGET (self), scroll_controller);
847

848
  zoom_gesture = gtk_gesture_zoom_new ();
849
850
851
  g_signal_connect_swapped (zoom_gesture, "begin", G_CALLBACK (on_zoom_gesture_begin), self);
  g_signal_connect_swapped (zoom_gesture, "update", G_CALLBACK (on_zoom_gesture_update), self);
  gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (zoom_gesture));
852

853
  motion_controller = gtk_event_controller_motion_new ();
854
855
  g_signal_connect_swapped (motion_controller, "motion", G_CALLBACK (on_motion_controller_motion), self);
  gtk_widget_add_controller (GTK_WIDGET (self), motion_controller);
856

James Westman's avatar
James Westman committed
857
858
859
860
861
862
863
  rotate_gesture = gtk_gesture_rotate_new ();
  g_signal_connect_swapped (rotate_gesture, "begin", G_CALLBACK (on_rotate_gesture_begin), self);
  g_signal_connect_swapped (rotate_gesture, "update", G_CALLBACK (on_rotate_gesture_update), self);
  gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (rotate_gesture));

  gtk_gesture_group (zoom_gesture, rotate_gesture);

864
  click_gesture = gtk_gesture_click_new ();
865
  gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (click_gesture), GDK_BUTTON_PRIMARY);
866
867
868
  g_signal_connect_swapped (click_gesture, "pressed", G_CALLBACK (on_click_gesture_pressed), self);
  gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (click_gesture));

869
870
871
  key_controller = gtk_event_controller_key_new ();
  gtk_widget_add_controller (GTK_WIDGET (self), key_controller);

872
  gtk_widget_set_overflow (GTK_WIDGET (self), GTK_OVERFLOW_HIDDEN);
873
874
875
}

/**
876
 * shumate_map_new:
877
 *
878
 * Creates an instance of #ShumateMap.
879
 *
880
 * Returns: a new #ShumateMap ready to be used as a #GtkWidget.
881
 */
882
883
ShumateMap *
shumate_map_new (void)
884
{
885
  return g_object_new (SHUMATE_TYPE_MAP, NULL);
886
887
888
}


889
890
ShumateMap *
shumate_map_new_simple (void)
891
{
892
  ShumateMap *view = g_object_new (SHUMATE_TYPE_MAP, NULL);
893
  g_autoptr(ShumateMapSourceRegistry) registry = NULL;
894
895
896
  ShumateMapSource *source;
  ShumateMapLayer *map_layer;
  ShumateViewport *viewport;
897
898

  viewport = shumate_map_get_viewport (view);
899
900
  registry = shumate_map_source_registry_new_with_defaults ();
  source = shumate_map_source_registry_get_by_id (registry, SHUMATE_MAP_SOURCE_OSM_MAPNIK);
901
902
  shumate_viewport_set_reference_map_source (viewport, source);
  map_layer = shumate_map_layer_new (source, viewport);
903
  shumate_map_add_layer (view, SHUMATE_LAYER (map_layer));
904

905
  return view;
906
907
908
}

/**
909
910
 * shumate_map_get_viewport:
 * @self: a #ShumateMap
911
 *
912
 * Get the #ShumateViewport used by this view.
913
 *
914
 * Returns: (transfer none): the #ShumateViewport
915
 */
916
ShumateViewport *
917
shumate_map_get_viewport (ShumateMap *self)
918
{
919
  g_return_val_if_fail (SHUMATE_IS_MAP (self), NULL);
920

James Westman's avatar
James Westman committed
921
  return self->viewport;
922
923
924
}

/**
925
926
 * shumate_map_center_on:
 * @self: a #ShumateMap
927
928
 * @latitude: the longitude to center the map at
 * @longitude: the longitude to center the map at
929
 *
930
 * Centers the map on these coordinates.
931
932
 */
void
933
934
935
shumate_map_center_on (ShumateMap *self,
                       double      latitude,
                       double      longitude)
936
{
937
  g_return_if_fail (SHUMATE_IS_MAP (self));
Marcus Lundblad's avatar
Marcus Lundblad committed
938

James Westman's avatar
James Westman committed
939
  shumate_location_set_location (SHUMATE_LOCATION (self->viewport), latitude, longitude);
940
941
942
}

/**
943
944
 * shumate_map_stop_go_to:
 * @self: a #ShumateMap
945
 *
946
947
 * Stop the go to animation.  The view will stay where it was when the
 * animation was stopped.
948
949
 */
void
950
shumate_map_stop_go_to (ShumateMap *self)
951
{
952
  g_return_if_fail (SHUMATE_IS_MAP (self));
953

James Westman's avatar
James Westman committed
954
  if (self->goto_context == NULL)
955
956
    return;

James Westman's avatar
James Westman committed
957
958
  gtk_widget_remove_tick_callback (GTK_WIDGET (self), self->goto_context->tick_id);
  g_clear_pointer (&self->goto_context, g_free);
959

960
  g_signal_emit (self, signals[ANIMATION_COMPLETED], go_to_quark, NULL);
961
962
963
964
}


/**
965
966
 * shumate_map_go_to:
 * @self: a #ShumateMap
967
968
 * @latitude: the longitude to center the map at
 * @longitude: the longitude to center the map at
969
 *
970
971
 * Move from the current position to these coordinates. All tiles in the
 * intermediate view WILL be loaded!
972
973
 */
void
974
975
976
shumate_map_go_to (ShumateMap *self,
                   double      latitude,
                   double      longitude)
James Westman's avatar
James Westman committed
977
978
979
980
981
982
983
{
  double zoom_level;

  g_return_if_fail (SHUMATE_IS_MAP (self));
  g_return_if_fail (latitude >= SHUMATE_MIN_LATITUDE && latitude <= SHUMATE_MAX_LATITUDE);
  g_return_if_fail (longitude >= SHUMATE_MIN_LONGITUDE && longitude <= SHUMATE_MAX_LONGITUDE);

James Westman's avatar
James Westman committed
984
  zoom_level = shumate_viewport_get_zoom_level (self->viewport);
James Westman's avatar
James Westman committed
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000

  shumate_map_go_to_full (self, latitude, longitude, zoom_level);
}


/**
 * shumate_map_go_to_full:
 * @self: a #ShumateMap
 * @latitude: the longitude to center the map at
 * @longitude: the longitude to center the map at
 * @zoom_level: the zoom level to end at
 *
 * Move from the current position to these coordinates and zoom to the given
 * zoom level. All tiles in the intermediate view WILL be loaded!
 */
void