GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 5.5% 25 / 0 / 457
Functions: 11.4% 4 / 0 / 35
Branches: 2.1% 4 / 0 / 190

shumate/shumate-map-layer.c
Line Branch Exec Source
1 /*
2 * Copyright 2020 Collabora, Ltd. (https://www.collabora.com)
3 * Copyright 2020 Corentin Noël <corentin.noel@collabora.com>
4 *
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Lesser General Public
7 * License as published by the Free Software Foundation; either
8 * version 2.1 of the License, or (at your option) any later version.
9 *
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Lesser General Public License for more details.
14 *
15 * You should have received a copy of the GNU Lesser General Public
16 * License along with this library; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18 */
19
20 #include "shumate-map-layer.h"
21 #include "shumate-marshal.h"
22 #include "shumate-memory-cache-private.h"
23 #include "shumate-tile-private.h"
24 #include "shumate-symbol-event.h"
25 #include "shumate-profiling-private.h"
26 #include "shumate-utils-private.h"
27 #include "shumate-inspector-settings-private.h"
28 #include "vector/shumate-vector-symbol-container-private.h"
29
30 /**
31 * ShumateMapLayer:
32 *
33 * A [class@Shumate.Layer] implementation that fetches tiles from a [class@Shumate.MapSource]
34 * and draws them as a grid.
35 *
36 * ## CSS nodes
37 *
38 * `ShumatePoint` has a single CSS node with the name “map-layer”.
39 */
40
41 struct _ShumateMapLayer
42 {
43 ShumateLayer parent_instance;
44
45 ShumateMapSource *map_source;
46
47 GHashTable *tile_children;
48
49 guint recompute_grid_idle_id;
50
51 float last_recompute_x, last_recompute_y;
52
53 ShumateMemoryCache *memcache;
54
55 gint64 profile_all_tiles_filled_begin;
56 gint64 profile_all_tiles_done_begin;
57
58 guint defer_callback_id;
59 double defer_latitude_y, defer_longitude_x, defer_zoom_level;
60 gint64 defer_frame_time;
61 gboolean deferring;
62
63 gboolean refreshing;
64 gboolean retrying_failed;
65
66 GSignalGroup *source_signal_group;
67
68 ShumateVectorSymbolContainer *symbols;
69 };
70
71
4/6
shumate_map_layer_class_intern_init:
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1 time.
shumate_map_layer_get_type:
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 7 taken 1 time.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 7 not taken.
4 G_DEFINE_TYPE (ShumateMapLayer, shumate_map_layer, SHUMATE_TYPE_LAYER)
72
73 enum
74 {
75 PROP_MAP_SOURCE = 1,
76 N_PROPERTIES
77 };
78
79 static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, };
80
81 enum
82 {
83 SYMBOL_CLICKED,
84 MAP_LOADED,
85 TILE_ERROR,
86 LAST_SIGNAL
87 };
88
89 static guint signals[LAST_SIGNAL] = { 0, };
90
91 /* This struct represents the location of a tile on the screen. It is the key
92 * for the hash table tile_children which stores all visible tiles.
93 *
94 * Note that, unlike the values given to ShumateTile, the x and y coordinates
95 * here are *not* wrapped. For example, a ShumateTile at level 3 might have
96 * coordinates of (7, 2) but have a TileGridPosition of (-1, 2). */
97
98
99 static int
100 ✗ positive_mod (int i, int n)
101 {
102 ✗ return (i % n + n) % n;
103 }
104
105 typedef struct {
106 ShumateMapLayer *self;
107 ShumateTile *current_tile;
108 ShumateTile *loading_tile;
109 GCancellable *cancellable;
110 ShumateGridPosition pos;
111 gboolean failed;
112 } TileChild;
113
114 static void
115 ✗ tile_child_free (TileChild *child)
116 {
117 ✗ g_clear_object (&child->current_tile);
118 ✗ g_clear_object (&child->loading_tile);
119 ✗ g_clear_object (&child->cancellable);
120 ✗ g_free (child);
121 ✗ }
122
123
124 typedef struct {
125 ShumateMapLayer *self;
126 TileChild *tile_child;
127 char *source_id;
128 } TileFilledData;
129
130 static void
131 ✗ tile_filled_data_free (TileFilledData *data)
132 {
133 ✗ g_clear_object (&data->self);
134 ✗ g_clear_pointer (&data->source_id, g_free);
135 ✗ g_free (data);
136 ✗ }
137 ✗ G_DEFINE_AUTOPTR_CLEANUP_FUNC (TileFilledData, tile_filled_data_free);
138
139
140 static void
141 ✗ add_symbols (ShumateMapLayer *self,
142 ShumateTile *tile,
143 ShumateGridPosition *pos)
144 {
145 ✗ GPtrArray *symbols;
146
147 ✗ g_assert (SHUMATE_IS_MAP_LAYER (self));
148 ✗ g_assert (SHUMATE_IS_TILE (tile));
149
150 ✗ shumate_vector_symbol_container_remove_symbols (self->symbols, pos->x, pos->y, pos->zoom);
151
152 ✗ if ((symbols = shumate_tile_get_symbols (tile)))
153 ✗ shumate_vector_symbol_container_add_symbols (self->symbols,
154 symbols,
155 pos->x,
156 pos->y,
157 pos->zoom);
158 ✗ }
159
160 static void recompute_grid (ShumateMapLayer *self);
161
162 static void
163 ✗ on_tile_notify_paintable (TileChild *tile_child,
164 G_GNUC_UNUSED GParamSpec *pspec,
165 ShumateTile *tile)
166 {
167 ✗ g_set_object (&tile_child->current_tile, tile);
168 ✗ add_symbols (tile_child->self, tile, &tile_child->pos);
169 ✗ gtk_widget_queue_draw (GTK_WIDGET (tile_child->self));
170 ✗ }
171
172 static void
173 ✗ on_tile_filled (GObject *source_object,
174 GAsyncResult *res,
175 gpointer user_data)
176 {
177 ✗ g_autoptr(TileFilledData) data = user_data;
178 ✗ g_autoptr(GError) error = NULL;
179 ✗ gboolean success;
180
181 ✗ success = shumate_map_source_fill_tile_finish (SHUMATE_MAP_SOURCE (source_object), res, &error);
182
183 ✗ if (!success && g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
184 ✗ return;
185
186 ✗ g_set_object (&data->tile_child->current_tile, data->tile_child->loading_tile);
187
188 ✗ data->tile_child->failed = !success;
189
190 ✗ if (!success)
191 {
192 ✗ g_signal_emit (data->self, signals[TILE_ERROR], 0, data->tile_child->current_tile, error);
193 }
194 else
195 {
196 ✗ shumate_memory_cache_store_tile (data->self->memcache,
197 data->tile_child->current_tile,
198 ✗ data->source_id);
199 }
200
201 ✗ recompute_grid (data->self);
202 }
203
204 static void
205 ✗ load_tile (ShumateMapLayer *self,
206 TileChild *tile_child)
207 {
208 ✗ g_autoptr(ShumateTile) tile = NULL;
209 ✗ const char *source_id = shumate_map_source_get_id (self->map_source);
210
211 ✗ guint64 source_rows = shumate_map_source_get_row_count (self->map_source, tile_child->pos.zoom);
212 ✗ guint64 source_columns = shumate_map_source_get_column_count (self->map_source, tile_child->pos.zoom);
213 ✗ int tile_size = shumate_map_source_get_tile_size (self->map_source);
214
215 ✗ tile = shumate_tile_new_full (positive_mod (tile_child->pos.x, source_columns),
216 ✗ positive_mod (tile_child->pos.y, source_rows),
217 tile_size,
218 ✗ tile_child->pos.zoom);
219 ✗ shumate_tile_set_scale_factor (tile, gtk_widget_get_scale_factor (GTK_WIDGET (self)));
220
221 ✗ g_cancellable_cancel (tile_child->cancellable);
222 ✗ g_clear_object (&tile_child->cancellable);
223
224 ✗ if (tile_child->loading_tile != NULL)
225 ✗ g_signal_handlers_disconnect_by_func (tile_child->loading_tile, on_tile_notify_paintable, tile_child);
226 ✗ g_signal_connect_swapped (tile, "notify::paintable", (GCallback)on_tile_notify_paintable, tile_child);
227
228 ✗ g_set_object (&tile_child->loading_tile, tile);
229
230 ✗ if (shumate_memory_cache_try_fill_tile (self->memcache, tile, source_id))
231 {
232 ✗ g_set_object (&tile_child->current_tile, tile);
233 ✗ tile_child->failed = FALSE;
234 }
235 else
236 {
237 ✗ TileFilledData *data = g_new0 (TileFilledData, 1);
238
239 ✗ tile_child->cancellable = g_cancellable_new ();
240
241 ✗ data->self = g_object_ref (self);
242 ✗ data->tile_child = tile_child;
243 ✗ data->source_id = g_strdup (source_id);
244
245 ✗ shumate_map_source_fill_tile_async (self->map_source, tile, tile_child->cancellable, on_tile_filled, data);
246 }
247 ✗ }
248
249 static TileChild *
250 ✗ add_tile (ShumateMapLayer *self,
251 ShumateGridPosition *pos)
252 {
253 ✗ TileChild *tile_child = g_new0 (TileChild, 1);
254
255 ✗ if (self->profile_all_tiles_filled_begin == 0)
256 ✗ self->profile_all_tiles_filled_begin = SHUMATE_PROFILE_CURRENT_TIME;
257 ✗ if (self->profile_all_tiles_done_begin == 0)
258 ✗ self->profile_all_tiles_done_begin = SHUMATE_PROFILE_CURRENT_TIME;
259
260 ✗ tile_child->self = self;
261 ✗ tile_child->pos = *pos;
262
263 ✗ g_hash_table_insert (self->tile_children, pos, tile_child);
264 ✗ gtk_widget_queue_draw (GTK_WIDGET (self));
265
266 ✗ load_tile (self, tile_child);
267
268 ✗ return tile_child;
269 }
270
271 static void
272 ✗ remove_tile (ShumateMapLayer *self,
273 TileChild *tile_child)
274 {
275 ✗ g_cancellable_cancel (tile_child->cancellable);
276
277 ✗ shumate_vector_symbol_container_remove_symbols (self->symbols, tile_child->pos.x, tile_child->pos.y, tile_child->pos.zoom);
278
279 ✗ g_signal_handlers_disconnect_by_func (tile_child->loading_tile, on_tile_notify_paintable, tile_child);
280 ✗ }
281
282 static double
283 ✗ get_effective_zoom_level (ShumateMapLayer *self)
284 {
285 ✗ double zoom_level = shumate_viewport_get_zoom_level (shumate_layer_get_viewport (SHUMATE_LAYER (self)));
286 ✗ ShumateMapSource *map_source = shumate_viewport_get_reference_map_source (shumate_layer_get_viewport (SHUMATE_LAYER (self)));
287
288 ✗ if (map_source)
289 {
290 ✗ double reference_tile_size = shumate_map_source_get_tile_size (map_source);
291 ✗ double our_tile_size = shumate_map_source_get_tile_size (self->map_source);
292 ✗ return log2 (reference_tile_size / our_tile_size) + zoom_level;
293 }
294 else
295 return zoom_level;
296 }
297
298 static gboolean
299 ✗ defer_tick_callback (GtkWidget *widget,
300 GdkFrameClock *frame_clock,
301 gpointer user_data)
302 {
303 ✗ ShumateMapLayer *self = SHUMATE_MAP_LAYER (widget);
304 ✗ self->defer_callback_id = 0;
305
306 ✗ recompute_grid (self);
307 ✗ return G_SOURCE_REMOVE;
308 }
309
310 static gboolean
311 ✗ should_defer (ShumateMapLayer *self)
312 {
313 /* If the map is moving quickly, we may defer loading tiles until it slows back
314 down. That way, we don't waste resources loading tiles that will likely be gone
315 before they are done loading. */
316
317 ✗ ShumateViewport *viewport = shumate_layer_get_viewport (SHUMATE_LAYER (self));
318 ✗ ShumateMapSource *map_source = self->map_source;
319 ✗ double zoom_level = get_effective_zoom_level (self);
320 ✗ double tile_size = shumate_map_source_get_tile_size_at_zoom (map_source, zoom_level);
321 ✗ double map_height = shumate_map_source_get_row_count (map_source, zoom_level) * tile_size;
322 ✗ double map_width = shumate_map_source_get_column_count (map_source, zoom_level) * tile_size;
323
324 ✗ double longitude_x = shumate_map_source_get_x (map_source, zoom_level, shumate_location_get_longitude (SHUMATE_LOCATION (viewport)));
325 ✗ double latitude_y = shumate_map_source_get_y (map_source, zoom_level, shumate_location_get_latitude (SHUMATE_LOCATION (viewport)));
326
327 ✗ double delta_x = self->defer_longitude_x * map_width - longitude_x;
328 ✗ double delta_y = self->defer_latitude_y * map_height - latitude_y;
329 ✗ double velocity = sqrt (delta_x * delta_x + delta_y * delta_y);
330 ✗ double width = gtk_widget_get_width (GTK_WIDGET (self));
331 ✗ double height = gtk_widget_get_height (GTK_WIDGET (self));
332 ✗ double diagonal = sqrt (width * width + height * height);
333 ✗ double zoom_velocity = self->defer_zoom_level - zoom_level;
334
335 ✗ gint64 frame_time;
336
337 ✗ if (!gtk_widget_get_realized (GTK_WIDGET (self)))
338 return FALSE;
339
340 ✗ frame_time = gdk_frame_clock_get_frame_time (gtk_widget_get_frame_clock (GTK_WIDGET (self)));
341 /* Only compare between frames, otherwise we might mistakenly think the
342 velocity is 0. */
343 ✗ if (frame_time == self->defer_frame_time)
344 ✗ return self->deferring;
345
346 ✗ if (velocity > diagonal * 0.25 || fabs (zoom_velocity) > 0.25)
347 {
348 ✗ if (self->defer_callback_id == 0)
349 ✗ self->defer_callback_id = gtk_widget_add_tick_callback (GTK_WIDGET (self), defer_tick_callback, NULL, NULL);
350
351 ✗ self->deferring = TRUE;
352 }
353 else
354 ✗ self->deferring = FALSE;
355
356 ✗ self->defer_latitude_y = latitude_y / map_height;
357 ✗ self->defer_longitude_x = longitude_x / map_width;
358 ✗ self->defer_zoom_level = zoom_level;
359 ✗ self->defer_frame_time = frame_time;
360
361 ✗ return self->deferring;
362 }
363
364 static void
365 ✗ recompute_grid (ShumateMapLayer *self)
366 {
367 /* Computes which tile positions are visible, ensures that all the right
368 * tiles are loaded, and removes tiles which are no longer visible. */
369
370 ✗ GHashTableIter iter;
371 ✗ gpointer key, value;
372
373 ✗ int width, height;
374 ✗ ShumateViewport *viewport;
375 ✗ int tile_size;
376 ✗ int zoom_level;
377 ✗ double latitude, longitude;
378 ✗ double latitude_y, longitude_x;
379 ✗ guint64 source_rows, source_columns;
380
381 ✗ double rotation;
382 ✗ int n_tiles = 0;
383
384 ✗ int size_x, size_y;
385
386 ✗ gint64 tile_initial_column, tile_initial_row, tile_final_column, tile_final_row;
387 ✗ int required_columns, required_rows;
388
389 ✗ gboolean defer;
390
391 ✗ gboolean all_filled = TRUE, all_done = TRUE, all_succeeded = TRUE;
392
393 ✗ if (self->map_source == NULL)
394 ✗ return;
395
396 ✗ width = gtk_widget_get_width (GTK_WIDGET (self));
397 ✗ height = gtk_widget_get_height (GTK_WIDGET (self));
398 ✗ viewport = shumate_layer_get_viewport (SHUMATE_LAYER (self));
399 ✗ tile_size = shumate_map_source_get_tile_size (self->map_source);
400 ✗ zoom_level = (int)floor (get_effective_zoom_level (self));
401 ✗ latitude = shumate_location_get_latitude (SHUMATE_LOCATION (viewport));
402 ✗ longitude = shumate_location_get_longitude (SHUMATE_LOCATION (viewport));
403 ✗ latitude_y = shumate_map_source_get_y (self->map_source, zoom_level, latitude);
404 ✗ longitude_x = shumate_map_source_get_x (self->map_source, zoom_level, longitude);
405 ✗ source_rows = shumate_map_source_get_row_count (self->map_source, zoom_level);
406 ✗ source_columns = shumate_map_source_get_column_count (self->map_source, zoom_level);
407
408 ✗ rotation = shumate_viewport_get_rotation (viewport);
409
410 ✗ size_x = MAX (
411 abs ((int) (cos (rotation) * width/2.0 - sin (rotation) * height/2.0)),
412 abs ((int) (cos (rotation) * -width/2.0 - sin (rotation) * height/2.0))
413 );
414 ✗ size_y = MAX (
415 abs ((int) (sin (rotation) * width/2.0 + cos (rotation) * height/2.0)),
416 abs ((int) (sin (rotation) * -width/2.0 + cos (rotation) * height/2.0))
417 );
418
419 // This is the (column, row) of the top left tile
420 ✗ tile_initial_column = floor ((longitude_x - size_x) / (double) tile_size) - 1;
421 ✗ tile_initial_row = floor ((latitude_y - size_y) / (double) tile_size) - 1;
422 ✗ tile_final_column = ceil ((longitude_x + size_x) / (double) tile_size) + 1;
423 ✗ tile_final_row = ceil ((latitude_y + size_y) / (double) tile_size) + 1;
424 ✗ required_columns = tile_final_column - tile_initial_column;
425 ✗ required_rows = tile_final_row - tile_initial_row;
426
427 ✗ defer = should_defer (self);
428
429 /* First, remove all the tiles that aren't in bounds, or that are on the
430 * wrong zoom level and haven't finished loading */
431 ✗ g_hash_table_iter_init (&iter, self->tile_children);
432 ✗ while (g_hash_table_iter_next (&iter, &key, &value))
433 {
434 ✗ ShumateGridPosition *pos = key;
435 ✗ TileChild *tile_child = value;
436 ✗ float size = powf (2, zoom_level - pos->zoom);
437 ✗ float x = pos->x * size;
438 ✗ float y = pos->y * size;
439
440 ✗ if (x + size <= tile_initial_column
441 ✗ || x >= tile_initial_column + required_columns
442 ✗ || y + size <= tile_initial_row
443 ✗ || y >= tile_initial_row + required_rows
444 ✗ || (pos->zoom != zoom_level && shumate_tile_get_state (tile_child->loading_tile) != SHUMATE_STATE_DONE))
445 {
446 ✗ remove_tile (self, tile_child);
447 ✗ g_hash_table_iter_remove (&iter);
448 }
449 }
450
451 /* Next, make sure every visible tile position has a matching ShumateTile. */
452 ✗ for (int x = tile_initial_column; x < tile_initial_column + required_columns; x ++)
453 {
454 ✗ for (int y = tile_initial_row; y < tile_initial_row + required_rows; y ++)
455 {
456 ✗ g_autoptr(ShumateGridPosition) pos = shumate_grid_position_new (x, y, zoom_level);
457 ✗ TileChild *tile_child = g_hash_table_lookup (self->tile_children, pos);
458
459 ✗ n_tiles ++;
460
461 ✗ if (!defer)
462 {
463 ✗ if (!tile_child)
464 ✗ tile_child = add_tile (self, g_steal_pointer (&pos));
465 ✗ else if (self->refreshing || (self->retrying_failed && tile_child->failed))
466 ✗ load_tile (self, tile_child);
467 }
468
469 ✗ if (tile_child == NULL || shumate_tile_get_paintable (tile_child->loading_tile) == NULL)
470 all_filled = FALSE;
471
472 ✗ if (tile_child == NULL || shumate_tile_get_state (tile_child->loading_tile) != SHUMATE_STATE_DONE)
473 all_done = FALSE;
474
475 ✗ if (tile_child != NULL && tile_child->failed)
476 ✗ all_succeeded = FALSE;
477 }
478 }
479
480 ✗ if (all_done && self->profile_all_tiles_done_begin > 0)
481 {
482 ✗ g_autofree char *desc = g_strdup_printf ("Visible tiles done (%d)", n_tiles);
483 ✗ SHUMATE_PROFILE_COLLECT (self->profile_all_tiles_done_begin, desc, NULL);
484 ✗ self->profile_all_tiles_done_begin = 0;
485
486 ✗ g_signal_emit (self, signals[MAP_LOADED], 0, !all_succeeded);
487 }
488 ✗ if (all_filled && self->profile_all_tiles_filled_begin > 0)
489 {
490 ✗ g_autofree char *desc = g_strdup_printf ("Visible tiles filled (%d)", n_tiles);
491 ✗ SHUMATE_PROFILE_COLLECT (self->profile_all_tiles_filled_begin, desc, NULL);
492 ✗ self->profile_all_tiles_filled_begin = 0;
493 }
494
495 /* If all the tiles on the current zoom level are filled, delete tiles on all
496 * other zoom levels */
497 ✗ if (all_done)
498 {
499 ✗ g_hash_table_iter_init (&iter, self->tile_children);
500 ✗ while (g_hash_table_iter_next (&iter, &key, &value))
501 {
502 ✗ ShumateGridPosition *pos = key;
503 ✗ TileChild *tile_child = value;
504
505 ✗ if (pos->zoom != zoom_level)
506 {
507 ✗ remove_tile (self, tile_child);
508 ✗ g_hash_table_iter_remove (&iter);
509 }
510 }
511 }
512
513 ✗ self->last_recompute_y = latitude_y / (double) (tile_size * source_rows);
514 ✗ self->last_recompute_x = longitude_x / (double) (tile_size * source_columns);
515
516 ✗ if (!defer)
517 {
518 ✗ self->refreshing = FALSE;
519 ✗ self->retrying_failed = FALSE;
520 }
521
522 ✗ gtk_widget_queue_draw (GTK_WIDGET (self));
523 }
524
525 static gboolean
526 ✗ recompute_grid_in_idle_cb (gpointer user_data)
527 {
528 ✗ ShumateMapLayer *self = user_data;
529
530 ✗ g_assert (SHUMATE_IS_MAP_LAYER (self));
531
532 ✗ recompute_grid (self);
533
534 ✗ self->recompute_grid_idle_id = 0;
535 ✗ return G_SOURCE_REMOVE;
536 }
537
538 static void
539 ✗ queue_recompute_grid_in_idle (ShumateMapLayer *self)
540 {
541 /* recompute_grid might add symbols to the map, which we can't do during
542 * certain operations, like size_allocate. So, in most cases, we schedule
543 * it to run later (but before the next frame) instead.
544 * Also, since we make sure to only have one queued recompute_grid at once,
545 * it has a nice side effect of running the function only once even if
546 * several viewport properties change at once. */
547
548 ✗ g_assert (SHUMATE_IS_MAP_LAYER (self));
549
550 ✗ if (self->recompute_grid_idle_id > 0)
551 return;
552
553 ✗ self->recompute_grid_idle_id = g_idle_add (recompute_grid_in_idle_cb, self);
554 ✗ g_source_set_name_by_id (self->recompute_grid_idle_id,
555 "[shumate] recompute_grid_in_idle_cb");
556 }
557
558
559 static void
560 ✗ on_viewport_changed (ShumateMapLayer *self,
561 GParamSpec *pspec,
562 ShumateViewport *view)
563 {
564 ✗ g_assert (SHUMATE_IS_MAP_LAYER (self));
565
566 ✗ queue_recompute_grid_in_idle (self);
567 ✗ }
568
569 static void
570 ✗ shumate_map_layer_set_property (GObject *object,
571 guint property_id,
572 const GValue *value,
573 GParamSpec *pspec)
574 {
575 ✗ ShumateMapLayer *self = SHUMATE_MAP_LAYER (object);
576
577 ✗ switch (property_id)
578 {
579 ✗ case PROP_MAP_SOURCE:
580 ✗ shumate_map_layer_set_map_source (self, g_value_get_object (value));
581 ✗ break;
582
583 ✗ default:
584 ✗ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
585 ✗ break;
586 }
587 ✗ }
588
589 static void
590 ✗ shumate_map_layer_get_property (GObject *object,
591 guint property_id,
592 GValue *value,
593 GParamSpec *pspec)
594 {
595 ✗ ShumateMapLayer *self = SHUMATE_MAP_LAYER (object);
596
597 ✗ switch (property_id)
598 {
599 ✗ case PROP_MAP_SOURCE:
600 ✗ g_value_set_object (value, self->map_source);
601 ✗ break;
602
603 ✗ default:
604 ✗ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
605 ✗ break;
606 }
607 ✗ }
608
609 static void
610 ✗ shumate_map_layer_dispose (GObject *object)
611 {
612 ✗ ShumateMapLayer *self = SHUMATE_MAP_LAYER (object);
613 ✗ ShumateViewport *viewport = shumate_layer_get_viewport (SHUMATE_LAYER (self));
614 ✗ GtkWidget *child;
615
616 ✗ g_signal_handlers_disconnect_by_data (viewport, self);
617 ✗ while ((child = gtk_widget_get_first_child (GTK_WIDGET (object))))
618 ✗ gtk_widget_unparent (child);
619
620 ✗ g_clear_handle_id (&self->recompute_grid_idle_id, g_source_remove);
621 ✗ g_clear_pointer (&self->tile_children, g_hash_table_unref);
622 ✗ g_clear_object (&self->map_source);
623 ✗ g_clear_object (&self->memcache);
624 ✗ g_clear_object (&self->source_signal_group);
625
626 ✗ G_OBJECT_CLASS (shumate_map_layer_parent_class)->dispose (object);
627 ✗ }
628
629 static void
630 ✗ on_symbol_clicked (ShumateMapLayer *self,
631 ShumateSymbolEvent *event,
632 ShumateVectorSymbolContainer *container)
633 {
634 ✗ g_signal_emit (self, signals[SYMBOL_CLICKED], 0, event);
635 ✗ }
636
637 static void
638 ✗ shumate_map_layer_constructed (GObject *object)
639 {
640 ✗ ShumateInspectorSettings *settings = shumate_inspector_settings_get_default ();
641 ✗ ShumateMapLayer *self = SHUMATE_MAP_LAYER (object);
642 ✗ ShumateViewport *viewport;
643
644 ✗ G_OBJECT_CLASS (shumate_map_layer_parent_class)->constructed (object);
645
646 ✗ viewport = shumate_layer_get_viewport (SHUMATE_LAYER (self));
647 ✗ g_signal_connect_swapped (viewport, "notify", G_CALLBACK (on_viewport_changed), self);
648
649 ✗ g_signal_connect_object (settings, "notify::show-tile-bounds", G_CALLBACK (queue_recompute_grid_in_idle), self, G_CONNECT_SWAPPED);
650
651 ✗ self->symbols = shumate_vector_symbol_container_new (self->map_source, viewport);
652 ✗ g_signal_connect_object (self->symbols, "symbol-clicked", G_CALLBACK (on_symbol_clicked), self, G_CONNECT_SWAPPED);
653 ✗ gtk_widget_set_parent (GTK_WIDGET (self->symbols), GTK_WIDGET (self));
654 ✗ }
655
656 static void
657 ✗ shumate_map_layer_size_allocate (GtkWidget *widget,
658 int width,
659 int height,
660 int baseline)
661 {
662 ✗ ShumateMapLayer *self = SHUMATE_MAP_LAYER (widget);
663 ✗ GtkAllocation child_allocation;
664
665 /* gtk_widget_measure needs to be called during size_allocate, but we don't
666 * care about the result here--the symbol container always gets the same
667 * size as the map layer */
668 ✗ gtk_widget_measure (GTK_WIDGET (self->symbols), GTK_ORIENTATION_VERTICAL, -1, NULL, NULL, NULL, NULL);
669
670 ✗ child_allocation.x = 0;
671 ✗ child_allocation.y = 0;
672 ✗ child_allocation.width = width;
673 ✗ child_allocation.height = height;
674 ✗ gtk_widget_size_allocate (GTK_WIDGET (self->symbols), &child_allocation, baseline);
675
676 /* Make sure the tile grid is up to date */
677 ✗ queue_recompute_grid_in_idle (self);
678 ✗ }
679
680 static void
681 ✗ shumate_map_layer_measure (GtkWidget *widget,
682 GtkOrientation orientation,
683 int for_size,
684 int *minimum,
685 int *natural,
686 int *minimum_baseline,
687 int *natural_baseline)
688 {
689 ✗ if (minimum)
690 ✗ *minimum = 0;
691
692 ✗ if (natural)
693 ✗ *natural = 0;
694 ✗ }
695
696 static double
697 ✗ snap_coordinate (double point,
698 double translate,
699 double size)
700 {
701 ✗ return round ((point - translate) / size) * size + translate;
702 }
703
704 static double
705 ✗ round_px (double x, double scale_factor)
706 {
707 ✗ return round (x * scale_factor) / scale_factor;
708 }
709
710 static void
711 ✗ shumate_map_layer_snapshot (GtkWidget *widget, GtkSnapshot *snapshot)
712 {
713 ✗ GHashTableIter iter;
714 ✗ gpointer key, value;
715
716 ✗ ShumateMapLayer *self = SHUMATE_MAP_LAYER (widget);
717 ✗ ShumateViewport *viewport = shumate_layer_get_viewport (SHUMATE_LAYER (self));
718 ✗ double zoom_level;
719 ✗ int width, height;
720 ✗ double rotation;
721 ✗ double latitude, longitude, latitude_y, longitude_x;
722 ✗ int tile_size;
723 ✗ double tile_size_for_zoom;
724 ✗ double map_width, map_height;
725 ✗ gboolean show_tile_bounds;
726 ✗ double scale_factor;
727
728 ✗ if (self->map_source == NULL)
729 {
730 ✗ gtk_widget_snapshot_child (widget, GTK_WIDGET (self->symbols), snapshot);
731 ✗ return;
732 }
733
734 ✗ zoom_level = get_effective_zoom_level (self);
735 ✗ width = gtk_widget_get_width (GTK_WIDGET (self));
736 ✗ height = gtk_widget_get_height (GTK_WIDGET (self));
737 ✗ rotation = shumate_viewport_get_rotation (viewport);
738 ✗ latitude = shumate_location_get_latitude (SHUMATE_LOCATION (viewport));
739 ✗ longitude = shumate_location_get_longitude (SHUMATE_LOCATION (viewport));
740 ✗ latitude_y = shumate_map_source_get_y (self->map_source, zoom_level, latitude);
741 ✗ longitude_x = shumate_map_source_get_x (self->map_source, zoom_level, longitude);
742 ✗ tile_size = shumate_map_source_get_tile_size (self->map_source);
743 ✗ tile_size_for_zoom = shumate_map_source_get_tile_size_at_zoom (self->map_source, zoom_level);
744 ✗ map_width = shumate_map_source_get_column_count (self->map_source, zoom_level) * tile_size_for_zoom;
745 ✗ map_height = shumate_map_source_get_row_count (self->map_source, zoom_level) * tile_size_for_zoom;
746 ✗ show_tile_bounds = shumate_inspector_settings_get_show_tile_bounds (shumate_inspector_settings_get_default ());
747 ✗ scale_factor = gtk_widget_get_scale_factor (widget);
748
749 /* Because Earth is round [citation needed], cylindrical projections like
750 * Mercator wrap around at the antimeridian. Moving across the antimeridian
751 * is the same as teleporting across the world: at one frame the longitude
752 * is just less than 180, and the next it's just more than -180.
753 *
754 * ShumateMapLayer doesn't handle teleportation well. Widgets can only be
755 * added/removed between frames, but animations are calculated during the
756 * frame. This means that by the time we know about the new viewport location,
757 * it's too late to move tiles around. recompute_grid(), which will fix the
758 * problem, won't be called until after the current frame.
759 *
760 * To fix this, recompute_grid() remembers the most recent location
761 * it saw. Then, to reduce "teleportation", here in snapshot() we render
762 * the "copy" of the new location that is closest to the one from
763 * recompute_grid(). This just means snapping the current location to a grid
764 * translated by the old location.
765 * */
766 ✗ longitude_x = snap_coordinate (self->last_recompute_x * map_width, longitude_x, map_width);
767 ✗ latitude_y = snap_coordinate (self->last_recompute_y * map_height, latitude_y, map_height);
768
769 /* Scale and rotate around the center of the view */
770 ✗ gtk_snapshot_save (snapshot);
771 ✗ gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (width / 2.0, height / 2.0));
772 ✗ gtk_snapshot_rotate (snapshot, rotation * 180 / G_PI);
773
774 ✗ gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (-width / 2.0, -height / 2.0));
775
776 ✗ g_hash_table_iter_init (&iter, self->tile_children);
777
778 ✗ while (g_hash_table_iter_next (&iter, &key, &value))
779 {
780 ✗ ShumateGridPosition *pos = key;
781 ✗ TileChild *tile_child = value;
782 ✗ GdkPaintable *paintable = NULL;
783 ✗ double size = tile_size * pow (2, zoom_level - pos->zoom);
784 ✗ double x = -(longitude_x - width/2.0) + size * pos->x;
785 ✗ double y = -(latitude_y - height/2.0) + size * pos->y;
786
787 ✗ if (tile_child->current_tile != NULL)
788 ✗ paintable = shumate_tile_get_paintable (tile_child->current_tile);
789
790 ✗ gtk_snapshot_save (snapshot);
791
792 ✗ gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (
793 round_px (x, scale_factor),
794 round_px (y, scale_factor)
795 ));
796
797 ✗ if (paintable != NULL)
798 {
799 ✗ gdk_paintable_snapshot (
800 paintable,
801 snapshot,
802 ✗ round_px (x + size, scale_factor) - round_px (x, scale_factor),
803 ✗ round_px (y + size, scale_factor) - round_px (y, scale_factor)
804 );
805 }
806
807 ✗ if (show_tile_bounds)
808 {
809 ✗ g_autofree char *text = NULL;
810 ✗ g_autoptr(PangoLayout) layout = NULL;
811 ✗ int x = shumate_tile_get_x (tile_child->loading_tile);
812 ✗ GdkRGBA color = {1, 0, 1, 1};
813
814 ✗ if (x == pos->x)
815 ✗ text = g_strdup_printf (" %d, %d, %d", pos->zoom, pos->x, pos->y);
816 else
817 ✗ text = g_strdup_printf (" %d, %d (%d), %d", pos->zoom, x, pos->x, pos->y);
818
819 ✗ layout = gtk_widget_create_pango_layout (widget, text);
820 ✗ gtk_snapshot_append_layout (snapshot, layout, &color);
821 ✗ gtk_snapshot_append_border (snapshot,
822 ✗ &GSK_ROUNDED_RECT_INIT (0, 0, size, size),
823 ✗ (float[]){1, 1, 1, 1},
824 ✗ (GdkRGBA[]) { color, color, color, color });
825 }
826
827 ✗ gtk_snapshot_restore (snapshot);
828 }
829
830 ✗ gtk_snapshot_restore (snapshot);
831
832 ✗ gtk_widget_snapshot_child (widget, GTK_WIDGET (self->symbols), snapshot);
833 }
834
835 static char *
836 ✗ shumate_map_layer_get_debug_text (ShumateLayer *layer)
837 {
838 ✗ ShumateMapLayer *self = SHUMATE_MAP_LAYER (layer);
839 ✗ g_autoptr(GString) string = g_string_new ("");
840 ✗ int n_loading = 0;
841 ✗ GHashTableIter iter;
842 ✗ TileChild *tile_child;
843 ✗ g_autofree char *symbol_debug = NULL;
844
845 ✗ g_hash_table_iter_init (&iter, self->tile_children);
846 ✗ while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&tile_child))
847 {
848 ✗ if (shumate_tile_get_state (tile_child->loading_tile) != SHUMATE_STATE_DONE)
849 ✗ n_loading ++;
850 }
851
852 ✗ g_string_append_printf (string,
853 "tiles: %d, %d loading\n",
854 g_hash_table_size (self->tile_children),
855 n_loading);
856
857 ✗ symbol_debug = shumate_vector_symbol_container_get_debug_text (self->symbols);
858 ✗ g_string_append (string, symbol_debug);
859
860 ✗ if (self->deferring)
861 ✗ g_string_append (string, "deferring\n");
862
863 ✗ return g_string_free_and_steal (g_steal_pointer (&string));
864 }
865
866 static void
867 1 shumate_map_layer_class_init (ShumateMapLayerClass *klass)
868 {
869 1 GObjectClass *object_class = G_OBJECT_CLASS (klass);
870 1 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
871 1 ShumateLayerClass *layer_class = SHUMATE_LAYER_CLASS (klass);
872
873 1 object_class->set_property = shumate_map_layer_set_property;
874 1 object_class->get_property = shumate_map_layer_get_property;
875 1 object_class->dispose = shumate_map_layer_dispose;
876 1 object_class->constructed = shumate_map_layer_constructed;
877
878 1 widget_class->size_allocate = shumate_map_layer_size_allocate;
879 1 widget_class->snapshot = shumate_map_layer_snapshot;
880 1 widget_class->measure = shumate_map_layer_measure;
881
882 1 layer_class->get_debug_text = shumate_map_layer_get_debug_text;
883
884 /**
885 * ShumateMapLayer:map-source:
886 *
887 * The source of the tiles this map layer displays.
888 */
889 2 obj_properties[PROP_MAP_SOURCE] =
890 1 g_param_spec_object ("map-source", NULL, NULL,
891 SHUMATE_TYPE_MAP_SOURCE,
892 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
893
894 1 g_object_class_install_properties (object_class,
895 N_PROPERTIES,
896 obj_properties);
897
898 /**
899 * ShumateMapLayer::symbol-clicked:
900 * @self: the [class@MapLayer] emitting the signal
901 * @event: a [class@SymbolEvent] with details about the clicked symbol.
902 *
903 * Emitted when a symbol in the map layer is clicked.
904 *
905 * Since: 1.1
906 */
907 2 signals[SYMBOL_CLICKED] =
908 1 g_signal_new ("symbol-clicked",
909 G_OBJECT_CLASS_TYPE (object_class),
910 G_SIGNAL_RUN_LAST,
911 0, NULL, NULL,
912 g_cclosure_marshal_VOID__OBJECT,
913 G_TYPE_NONE,
914 1,
915 SHUMATE_TYPE_SYMBOL_EVENT);
916
917 /**
918 * ShumateMapLayer::map-loaded:
919 * @self: the [class@MapLayer] emitting the signal
920 * @errors: `TRUE` if any visible tiles failed to load
921 *
922 * Emitted when all the tiles in the map view are finished loading.
923 *
924 * As the map is panned or zoomed, this signal may be emitted multiple times.
925 *
926 * Since: 1.4
927 */
928 2 signals[MAP_LOADED] =
929 1 g_signal_new ("map-loaded",
930 G_OBJECT_CLASS_TYPE (object_class),
931 G_SIGNAL_RUN_LAST,
932 0, NULL, NULL,
933 g_cclosure_marshal_VOID__BOOLEAN,
934 G_TYPE_NONE,
935 1,
936 G_TYPE_BOOLEAN);
937
938 /**
939 * ShumateMapLayer::tile-error:
940 * @self: the [class@MapLayer] emitting the signal
941 * @tile: the [class@Tile] that failed to load
942 * @error: the error that occurred
943 *
944 * Emitted when a tile fails to load.
945 *
946 * Since: 1.4
947 */
948 2 signals[TILE_ERROR] =
949 1 g_signal_new ("tile-error",
950 G_OBJECT_CLASS_TYPE (object_class),
951 G_SIGNAL_RUN_LAST,
952 0, NULL, NULL,
953 _shumate_marshal_VOID__OBJECT_BOXED,
954 G_TYPE_NONE,
955 2,
956 SHUMATE_TYPE_TILE,
957 G_TYPE_ERROR);
958 1 }
959
960 static void
961 ✗ on_source_modified (ShumateMapLayer *self,
962 G_GNUC_UNUSED ShumateMapSource *source)
963 {
964 ✗ shumate_map_layer_refresh (self);
965 ✗ }
966
967 static void
968 ✗ shumate_map_layer_init (ShumateMapLayer *self)
969 {
970 ✗ self->tile_children = g_hash_table_new_full (
971 shumate_grid_position_hash,
972 shumate_grid_position_equal,
973 shumate_grid_position_free,
974 (GDestroyNotify)tile_child_free
975 );
976 ✗ self->memcache = shumate_memory_cache_new_full (100);
977
978 ✗ self->source_signal_group = g_signal_group_new (SHUMATE_TYPE_MAP_SOURCE);
979 ✗ g_signal_group_connect_object (self->source_signal_group, "modified", G_CALLBACK (on_source_modified), self, G_CONNECT_SWAPPED);
980 ✗ g_object_bind_property (self, "map-source", self->source_signal_group, "target", G_BINDING_SYNC_CREATE);
981 ✗ }
982
983 ShumateMapLayer *
984 ✗ shumate_map_layer_new (ShumateMapSource *map_source,
985 ShumateViewport *viewport)
986 {
987 ✗ return g_object_new (SHUMATE_TYPE_MAP_LAYER,
988 "map-source", map_source,
989 "viewport", viewport,
990 NULL);
991 }
992
993
994 /**
995 * shumate_map_layer_get_map_source:
996 * @self: a [class@MapLayer]
997 *
998 * Gets the [class@MapSource] providing tiles for this map layer.
999 *
1000 * Returns: (transfer none): the [class@MapSource]
1001 * Since: 1.7
1002 */
1003 ShumateMapSource *
1004 ✗ shumate_map_layer_get_map_source (ShumateMapLayer *self)
1005 {
1006 ✗ g_return_val_if_fail (SHUMATE_IS_MAP_LAYER (self), NULL);
1007 ✗ return self->map_source;
1008 }
1009
1010 /**
1011 * shumate_map_layer_set_map_source:
1012 * @self: a [class@MapLayer]
1013 * @map_source: a [class@MapSource]
1014 *
1015 * Sets the [class@MapSource] that provides tiles for this map layer to display.
1016 *
1017 * Setting a new map source will refresh all tiles from the new source.
1018 *
1019 * Since: 1.7
1020 */
1021 void
1022 ✗ shumate_map_layer_set_map_source (ShumateMapLayer *self,
1023 ShumateMapSource *map_source)
1024 {
1025 ✗ gboolean tile_size_differs = TRUE;
1026
1027 ✗ g_return_if_fail (SHUMATE_IS_MAP_LAYER (self));
1028 ✗ g_return_if_fail (map_source == NULL || SHUMATE_IS_MAP_SOURCE (map_source));
1029
1030 ✗ if (self->map_source == map_source)
1031 return;
1032
1033 ✗ if (self->map_source != NULL && map_source != NULL)
1034 ✗ tile_size_differs = shumate_map_source_get_tile_size (self->map_source) != shumate_map_source_get_tile_size (map_source);
1035
1036 ✗ g_set_object (&self->map_source, map_source);
1037
1038 ✗ shumate_vector_symbol_container_set_map_source (self->symbols, map_source);
1039
1040 ✗ if (tile_size_differs)
1041 {
1042 /* If the tile size differs, remove all existing tiles, since they are invalid on the new grid. If the sizes are
1043 the same, we can keep the existing tiles until new ones are loaded over them, which eliminates the flash of
1044 background color while new tiles load. */
1045
1046 ✗ GHashTableIter iter;
1047 ✗ gpointer key, value;
1048
1049 ✗ g_hash_table_iter_init (&iter, self->tile_children);
1050 ✗ while (g_hash_table_iter_next (&iter, &key, &value))
1051 {
1052 ✗ remove_tile (self, (TileChild *) value);
1053 ✗ g_hash_table_iter_remove (&iter);
1054 }
1055 }
1056
1057 ✗ shumate_map_layer_refresh (self);
1058
1059 ✗ g_object_notify_by_pspec (G_OBJECT (self), obj_properties[PROP_MAP_SOURCE]);
1060 }
1061
1062 /**
1063 * shumate_map_layer_refresh:
1064 * @self: a [class@MapLayer]
1065 *
1066 * Refreshes all tiles from the current map source.
1067 *
1068 * Since: 1.7
1069 */
1070 void
1071 ✗ shumate_map_layer_refresh (ShumateMapLayer *self)
1072 {
1073 ✗ g_return_if_fail (SHUMATE_IS_MAP_LAYER (self));
1074
1075 ✗ self->refreshing = TRUE;
1076
1077 ✗ shumate_memory_cache_clean (self->memcache);
1078 ✗ queue_recompute_grid_in_idle (self);
1079 }
1080
1081 /**
1082 * shumate_map_layer_retry_failed:
1083 * @self: a [class@MapLayer]
1084 *
1085 * Attempts to reload any tiles in an error state.
1086 *
1087 * This can be used to recover from temporary errors. For example, you could use [iface@Gio.NetworkMonitor] to listen
1088 * for changes in network connectivity, and call this function when the network becomes available again.
1089 *
1090 * Since: 1.7
1091 */
1092 void
1093 ✗ shumate_map_layer_retry_failed (ShumateMapLayer *self)
1094 {
1095 ✗ g_return_if_fail (SHUMATE_IS_MAP_LAYER (self));
1096
1097 ✗ self->retrying_failed = TRUE;
1098
1099 ✗ queue_recompute_grid_in_idle (self);
1100 }
1101