Branch data Line data Source code
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-memory-cache-private.h"
22 : : #include "shumate-tile-private.h"
23 : : #include "shumate-symbol-event.h"
24 : : #include "shumate-profiling-private.h"
25 : : #include "shumate-utils-private.h"
26 : : #include "shumate-inspector-settings-private.h"
27 : :
28 : : #ifdef SHUMATE_HAS_VECTOR_RENDERER
29 : : # include "vector/shumate-vector-symbol-container-private.h"
30 : : #endif
31 : :
32 : : /**
33 : : * ShumateMapLayer:
34 : : *
35 : : * A [class@Shumate.Layer] implementation that fetches tiles from a [class@Shumate.MapSource]
36 : : * and draws them as a grid.
37 : : */
38 : :
39 : : struct _ShumateMapLayer
40 : : {
41 : : ShumateLayer parent_instance;
42 : :
43 : : ShumateMapSource *map_source;
44 : :
45 : : GHashTable *tile_children;
46 : : GHashTable *tile_fill;
47 : :
48 : : guint recompute_grid_idle_id;
49 : :
50 : : float last_recompute_x, last_recompute_y;
51 : :
52 : : ShumateMemoryCache *memcache;
53 : :
54 : : gint64 profile_all_tiles_filled_begin;
55 : : gint64 profile_all_tiles_done_begin;
56 : :
57 : : guint defer_callback_id;
58 : : double defer_latitude_y, defer_longitude_x, defer_zoom_level;
59 : : gint64 defer_frame_time;
60 : : gboolean deferring;
61 : :
62 : : #ifdef SHUMATE_HAS_VECTOR_RENDERER
63 : : ShumateVectorSymbolContainer *symbols;
64 : : #endif
65 : : };
66 : :
67 [ + + + - ]: 3 : G_DEFINE_TYPE (ShumateMapLayer, shumate_map_layer, SHUMATE_TYPE_LAYER)
68 : :
69 : : enum
70 : : {
71 : : PROP_MAP_SOURCE = 1,
72 : : N_PROPERTIES
73 : : };
74 : :
75 : : static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, };
76 : :
77 : : enum
78 : : {
79 : : SYMBOL_CLICKED,
80 : : LAST_SIGNAL
81 : : };
82 : :
83 : : static guint signals[LAST_SIGNAL] = { 0, };
84 : :
85 : : /* This struct represents the location of a tile on the screen. It is the key
86 : : * for the hash table tile_children which stores all visible tiles.
87 : : *
88 : : * Note that, unlike the values given to ShumateTile, the x and y coordinates
89 : : * here are *not* wrapped. For example, a ShumateTile at level 3 might have
90 : : * coordinates of (7, 2) but have a TileGridPosition of (-1, 2). */
91 : :
92 : :
93 : : static int
94 : 0 : positive_mod (int i, int n)
95 : : {
96 : 0 : return (i % n + n) % n;
97 : : }
98 : :
99 : : typedef struct {
100 : : ShumateMapLayer *self;
101 : : ShumateTile *tile;
102 : : char *source_id;
103 : : ShumateGridPosition pos;
104 : : } TileFilledData;
105 : :
106 : : static void
107 : 0 : tile_filled_data_free (TileFilledData *data)
108 : : {
109 [ # # ]: 0 : g_clear_object (&data->self);
110 [ # # ]: 0 : g_clear_object (&data->tile);
111 [ # # ]: 0 : g_clear_pointer (&data->source_id, g_free);
112 : 0 : g_free (data);
113 : 0 : }
114 : 0 : G_DEFINE_AUTOPTR_CLEANUP_FUNC (TileFilledData, tile_filled_data_free);
115 : :
116 : :
117 : : static void
118 : 0 : add_symbols (ShumateMapLayer *self,
119 : : ShumateTile *tile,
120 : : ShumateGridPosition *pos)
121 : : {
122 : : #ifdef SHUMATE_HAS_VECTOR_RENDERER
123 : 0 : GPtrArray *symbols;
124 : :
125 [ # # ]: 0 : g_assert (SHUMATE_IS_MAP_LAYER (self));
126 [ # # ]: 0 : g_assert (SHUMATE_IS_TILE (tile));
127 : :
128 [ # # ]: 0 : if ((symbols = shumate_tile_get_symbols (tile)))
129 : 0 : shumate_vector_symbol_container_add_symbols (self->symbols,
130 : : symbols,
131 : : pos->x,
132 : : pos->y,
133 : : pos->zoom);
134 : : #endif
135 : 0 : }
136 : :
137 : : static void recompute_grid (ShumateMapLayer *self);
138 : :
139 : : static void
140 : 0 : on_tile_notify_state (ShumateMapLayer *self,
141 : : G_GNUC_UNUSED GParamSpec *pspec,
142 : : ShumateTile *tile)
143 : : {
144 : 0 : gtk_widget_queue_draw (GTK_WIDGET (self));
145 : 0 : }
146 : :
147 : : static void
148 : 0 : on_tile_filled (GObject *source_object,
149 : : GAsyncResult *res,
150 : : gpointer user_data)
151 : : {
152 : 0 : g_autoptr(TileFilledData) data = user_data;
153 [ # # ]: 0 : g_autoptr(GError) error = NULL;
154 : 0 : gboolean success;
155 : :
156 : 0 : success = shumate_map_source_fill_tile_finish (SHUMATE_MAP_SOURCE (source_object), res, &error);
157 : :
158 : : // TODO: Report the error
159 [ # # ]: 0 : if (!success)
160 [ # # ]: 0 : return;
161 : :
162 : 0 : add_symbols (data->self, data->tile, &data->pos);
163 : :
164 : 0 : shumate_memory_cache_store_tile (data->self->memcache,
165 : : data->tile,
166 : 0 : data->source_id);
167 : :
168 [ # # ]: 0 : recompute_grid (data->self);
169 : : }
170 : :
171 : : static void
172 : 0 : add_tile (ShumateMapLayer *self,
173 : : ShumateTile *tile,
174 : : ShumateGridPosition *pos)
175 : : {
176 : 0 : const char *source_id = shumate_map_source_get_id (self->map_source);
177 : :
178 : 0 : self->profile_all_tiles_filled_begin = SHUMATE_PROFILE_CURRENT_TIME;
179 : 0 : self->profile_all_tiles_done_begin = SHUMATE_PROFILE_CURRENT_TIME;
180 : :
181 [ # # ]: 0 : if (shumate_memory_cache_try_fill_tile (self->memcache, tile, source_id))
182 : : {
183 : 0 : add_symbols (self, tile, pos);
184 : : }
185 : : else
186 : : {
187 : 0 : GCancellable *cancellable = g_cancellable_new ();
188 : 0 : TileFilledData *data = g_new0 (TileFilledData, 1);
189 : 0 : data->self = g_object_ref (self);
190 : 0 : data->tile = g_object_ref (tile);
191 [ # # ]: 0 : data->source_id = g_strdup (source_id);
192 : 0 : data->pos = *pos;
193 : :
194 : 0 : shumate_tile_set_paintable (tile, NULL);
195 : 0 : shumate_map_source_fill_tile_async (self->map_source, tile, cancellable, on_tile_filled, data);
196 : 0 : g_hash_table_insert (self->tile_fill, g_object_ref (tile), cancellable);
197 : : }
198 : :
199 : 0 : g_hash_table_insert (self->tile_children, pos, tile);
200 : 0 : gtk_widget_queue_draw (GTK_WIDGET (self));
201 : 0 : g_signal_connect_object (tile, "notify::state", (GCallback)on_tile_notify_state, self, G_CONNECT_SWAPPED);
202 : 0 : }
203 : :
204 : : static void
205 : 0 : remove_tile (ShumateMapLayer *self,
206 : : ShumateTile *tile,
207 : : ShumateGridPosition *pos)
208 : : {
209 : 0 : GCancellable *cancellable = g_hash_table_lookup (self->tile_fill, tile);
210 [ # # ]: 0 : if (cancellable)
211 : : {
212 : 0 : g_cancellable_cancel (cancellable);
213 : 0 : g_hash_table_remove (self->tile_fill, tile);
214 : : }
215 : :
216 : : #ifdef SHUMATE_HAS_VECTOR_RENDERER
217 : 0 : shumate_vector_symbol_container_remove_symbols (self->symbols, pos->x, pos->y, pos->zoom);
218 : : #endif
219 : :
220 : 0 : g_signal_handlers_disconnect_by_func (tile, on_tile_notify_state, self);
221 : 0 : }
222 : :
223 : : static double
224 : 0 : get_effective_zoom_level (ShumateMapLayer *self)
225 : : {
226 : 0 : double zoom_level = shumate_viewport_get_zoom_level (shumate_layer_get_viewport (SHUMATE_LAYER (self)));
227 : 0 : ShumateMapSource *map_source = shumate_viewport_get_reference_map_source (shumate_layer_get_viewport (SHUMATE_LAYER (self)));
228 : :
229 [ # # ]: 0 : if (map_source)
230 : : {
231 : 0 : double reference_tile_size = shumate_map_source_get_tile_size (map_source);
232 : 0 : double our_tile_size = shumate_map_source_get_tile_size (self->map_source);
233 : 0 : return log2 (reference_tile_size / our_tile_size) + zoom_level;
234 : : }
235 : : else
236 : : return zoom_level;
237 : : }
238 : :
239 : : static gboolean
240 : 0 : defer_tick_callback (GtkWidget *widget,
241 : : GdkFrameClock *frame_clock,
242 : : gpointer user_data)
243 : : {
244 : 0 : ShumateMapLayer *self = SHUMATE_MAP_LAYER (widget);
245 : 0 : self->defer_callback_id = 0;
246 : :
247 : 0 : recompute_grid (self);
248 : 0 : return G_SOURCE_REMOVE;
249 : : }
250 : :
251 : : static gboolean
252 : 0 : should_defer (ShumateMapLayer *self)
253 : : {
254 : : /* If the map is moving quickly, we may defer loading tiles until it slows back
255 : : down. That way, we don't waste resources loading tiles that will likely be gone
256 : : before they are done loading. */
257 : :
258 : 0 : ShumateViewport *viewport = shumate_layer_get_viewport (SHUMATE_LAYER (self));
259 : 0 : ShumateMapSource *map_source = self->map_source;
260 : 0 : double zoom_level = get_effective_zoom_level (self);
261 : 0 : double tile_size = shumate_map_source_get_tile_size_at_zoom (map_source, zoom_level);
262 : 0 : double map_height = shumate_map_source_get_row_count (map_source, zoom_level) * tile_size;
263 : 0 : double map_width = shumate_map_source_get_column_count (map_source, zoom_level) * tile_size;
264 : :
265 : 0 : double longitude_x = shumate_map_source_get_x (map_source, zoom_level, shumate_location_get_longitude (SHUMATE_LOCATION (viewport)));
266 : 0 : double latitude_y = shumate_map_source_get_y (map_source, zoom_level, shumate_location_get_latitude (SHUMATE_LOCATION (viewport)));
267 : :
268 : 0 : double delta_x = self->defer_longitude_x * map_width - longitude_x;
269 : 0 : double delta_y = self->defer_latitude_y * map_height - latitude_y;
270 : 0 : double velocity = sqrt (delta_x * delta_x + delta_y * delta_y);
271 : 0 : double width = gtk_widget_get_width (GTK_WIDGET (self));
272 : 0 : double height = gtk_widget_get_height (GTK_WIDGET (self));
273 : 0 : double diagonal = sqrt (width * width + height * height);
274 : 0 : double zoom_velocity = self->defer_zoom_level - zoom_level;
275 : :
276 : 0 : gint64 frame_time;
277 : :
278 [ # # ]: 0 : if (!gtk_widget_get_realized (GTK_WIDGET (self)))
279 : : return FALSE;
280 : :
281 : 0 : frame_time = gdk_frame_clock_get_frame_time (gtk_widget_get_frame_clock (GTK_WIDGET (self)));
282 : : /* Only compare between frames, otherwise we might mistakenly think the
283 : : velocity is 0. */
284 [ # # ]: 0 : if (frame_time == self->defer_frame_time)
285 : 0 : return self->deferring;
286 : :
287 [ # # # # ]: 0 : if (velocity > diagonal * 0.25 || fabs (zoom_velocity) > 0.25)
288 : : {
289 [ # # ]: 0 : if (self->defer_callback_id == 0)
290 : 0 : self->defer_callback_id = gtk_widget_add_tick_callback (GTK_WIDGET (self), defer_tick_callback, NULL, NULL);
291 : :
292 : 0 : self->deferring = TRUE;
293 : : }
294 : : else
295 : 0 : self->deferring = FALSE;
296 : :
297 : 0 : self->defer_latitude_y = latitude_y / map_height;
298 : 0 : self->defer_longitude_x = longitude_x / map_width;
299 : 0 : self->defer_zoom_level = zoom_level;
300 : 0 : self->defer_frame_time = frame_time;
301 : :
302 : 0 : return self->deferring;
303 : : }
304 : :
305 : : static void
306 : 0 : recompute_grid (ShumateMapLayer *self)
307 : : {
308 : : /* Computes which tile positions are visible, ensures that all the right
309 : : * tiles are loaded, and removes tiles which are no longer visible. */
310 : :
311 : 0 : GHashTableIter iter;
312 : 0 : gpointer key, value;
313 : :
314 : 0 : int width = gtk_widget_get_width (GTK_WIDGET (self));
315 : 0 : int height = gtk_widget_get_height (GTK_WIDGET (self));
316 : 0 : ShumateViewport *viewport = shumate_layer_get_viewport (SHUMATE_LAYER (self));
317 : 0 : int tile_size = shumate_map_source_get_tile_size (self->map_source);
318 : 0 : int zoom_level = (int)floor (get_effective_zoom_level (self));
319 : 0 : double latitude = shumate_location_get_latitude (SHUMATE_LOCATION (viewport));
320 : 0 : double longitude = shumate_location_get_longitude (SHUMATE_LOCATION (viewport));
321 : 0 : int latitude_y = shumate_map_source_get_y (self->map_source, zoom_level, latitude);
322 : 0 : int longitude_x = shumate_map_source_get_x (self->map_source, zoom_level, longitude);
323 : 0 : int source_rows = shumate_map_source_get_row_count (self->map_source, zoom_level);
324 : 0 : int source_columns = shumate_map_source_get_column_count (self->map_source, zoom_level);
325 : :
326 : 0 : double rotation = shumate_viewport_get_rotation (viewport);
327 : 0 : int n_tiles = 0;
328 : :
329 : 0 : int size_x = MAX (
330 : : abs ((int) (cos (rotation) * width/2.0 - sin (rotation) * height/2.0)),
331 : : abs ((int) (cos (rotation) * -width/2.0 - sin (rotation) * height/2.0))
332 : : );
333 : 0 : int size_y = MAX (
334 : : abs ((int) (sin (rotation) * width/2.0 + cos (rotation) * height/2.0)),
335 : : abs ((int) (sin (rotation) * -width/2.0 + cos (rotation) * height/2.0))
336 : : );
337 : :
338 : : // This is the (column, row) of the top left tile
339 : 0 : int tile_initial_column = floor ((longitude_x - size_x) / (double) tile_size) - 1;
340 : 0 : int tile_initial_row = floor ((latitude_y - size_y) / (double) tile_size) - 1;
341 : 0 : int tile_final_column = ceil ((longitude_x + size_x) / (double) tile_size) + 1;
342 : 0 : int tile_final_row = ceil ((latitude_y + size_y) / (double) tile_size) + 1;
343 : 0 : int required_columns = tile_final_column - tile_initial_column;
344 : 0 : int required_rows = tile_final_row - tile_initial_row;
345 : :
346 : 0 : gboolean defer = should_defer (self);
347 : :
348 : 0 : gboolean all_filled = TRUE, all_done = TRUE;
349 : :
350 : : /* First, remove all the tiles that aren't in bounds, or that are on the
351 : : * wrong zoom level and haven't finished loading */
352 : 0 : g_hash_table_iter_init (&iter, self->tile_children);
353 [ # # ]: 0 : while (g_hash_table_iter_next (&iter, &key, &value))
354 : : {
355 : 0 : ShumateGridPosition *pos = key;
356 : 0 : ShumateTile *tile = value;
357 : 0 : float size = powf (2, zoom_level - pos->zoom);
358 : 0 : float x = pos->x * size;
359 : 0 : float y = pos->y * size;
360 : :
361 [ # # ]: 0 : if (x + size <= tile_initial_column
362 [ # # ]: 0 : || x >= tile_initial_column + required_columns
363 [ # # ]: 0 : || y + size <= tile_initial_row
364 [ # # ]: 0 : || y >= tile_initial_row + required_rows
365 [ # # # # ]: 0 : || (pos->zoom != zoom_level && shumate_tile_get_state (tile) != SHUMATE_STATE_DONE))
366 : : {
367 : 0 : remove_tile (self, tile, pos);
368 : 0 : g_hash_table_iter_remove (&iter);
369 : : }
370 : : }
371 : :
372 : : /* Next, make sure every visible tile position has a matching ShumateTile. */
373 [ # # ]: 0 : for (int x = tile_initial_column; x < tile_initial_column + required_columns; x ++)
374 : : {
375 [ # # ]: 0 : for (int y = tile_initial_row; y < tile_initial_row + required_rows; y ++)
376 : : {
377 : 0 : g_autoptr(ShumateGridPosition) pos = shumate_grid_position_new (x, y, zoom_level);
378 : 0 : ShumateTile *tile = g_hash_table_lookup (self->tile_children, pos);
379 : :
380 : 0 : n_tiles ++;
381 : :
382 [ # # ]: 0 : if (!tile && !defer)
383 : : {
384 : 0 : tile = shumate_tile_new_full (positive_mod (x, source_columns), positive_mod (y, source_rows), tile_size, zoom_level);
385 : 0 : shumate_tile_set_scale_factor (tile, gtk_widget_get_scale_factor (GTK_WIDGET (self)));
386 : 0 : add_tile (self, tile, g_steal_pointer (&pos));
387 : : }
388 : :
389 [ # # # # ]: 0 : if (tile == NULL || shumate_tile_get_paintable (tile) == NULL)
390 : : all_filled = FALSE;
391 : :
392 [ # # ]: 0 : if (tile == NULL || shumate_tile_get_state (tile) != SHUMATE_STATE_DONE)
393 : : all_done = FALSE;
394 : : }
395 : : }
396 : :
397 [ # # # # ]: 0 : if (all_done && self->profile_all_tiles_done_begin > 0)
398 : : {
399 : 0 : g_autofree char *desc = g_strdup_printf ("Visible tiles done (%d)", n_tiles);
400 : 0 : SHUMATE_PROFILE_COLLECT (self->profile_all_tiles_done_begin, desc, NULL);
401 : 0 : self->profile_all_tiles_done_begin = 0;
402 : : }
403 [ # # # # ]: 0 : if (all_filled && self->profile_all_tiles_filled_begin > 0)
404 : : {
405 : 0 : g_autofree char *desc = g_strdup_printf ("Visible tiles filled (%d)", n_tiles);
406 : 0 : SHUMATE_PROFILE_COLLECT (self->profile_all_tiles_filled_begin, desc, NULL);
407 : 0 : self->profile_all_tiles_filled_begin = 0;
408 : : }
409 : :
410 : : /* If all the tiles on the current zoom level are filled, delete tiles on all
411 : : * other zoom levels */
412 [ # # ]: 0 : if (all_done)
413 : : {
414 : 0 : g_hash_table_iter_init (&iter, self->tile_children);
415 [ # # ]: 0 : while (g_hash_table_iter_next (&iter, &key, &value))
416 : : {
417 : 0 : ShumateGridPosition *pos = key;
418 : 0 : ShumateTile *tile = value;
419 : :
420 [ # # ]: 0 : if (pos->zoom != zoom_level)
421 : : {
422 : 0 : remove_tile (self, tile, pos);
423 : 0 : g_hash_table_iter_remove (&iter);
424 : : }
425 : : }
426 : : }
427 : :
428 : 0 : self->last_recompute_y = latitude_y / (double) (tile_size * source_rows);
429 : 0 : self->last_recompute_x = longitude_x / (double) (tile_size * source_columns);
430 : :
431 : 0 : gtk_widget_queue_draw (GTK_WIDGET (self));
432 : 0 : }
433 : :
434 : : static gboolean
435 : 0 : recompute_grid_in_idle_cb (gpointer user_data)
436 : : {
437 : 0 : ShumateMapLayer *self = user_data;
438 : :
439 [ # # ]: 0 : g_assert (SHUMATE_IS_MAP_LAYER (self));
440 : :
441 : 0 : recompute_grid (self);
442 : :
443 : 0 : self->recompute_grid_idle_id = 0;
444 : 0 : return G_SOURCE_REMOVE;
445 : : }
446 : :
447 : : static void
448 : 0 : queue_recompute_grid_in_idle (ShumateMapLayer *self)
449 : : {
450 : : /* recompute_grid might add symbols to the map, which we can't do during
451 : : * certain operations, like size_allocate. So, in most cases, we schedule
452 : : * it to run later (but before the next frame) instead.
453 : : * Also, since we make sure to only have one queued recompute_grid at once,
454 : : * it has a nice side effect of running the function only once even if
455 : : * several viewport properties change at once. */
456 : :
457 [ # # ]: 0 : g_assert (SHUMATE_IS_MAP_LAYER (self));
458 : :
459 [ # # ]: 0 : if (self->recompute_grid_idle_id > 0)
460 : : return;
461 : :
462 : 0 : self->recompute_grid_idle_id = g_idle_add (recompute_grid_in_idle_cb, self);
463 : 0 : g_source_set_name_by_id (self->recompute_grid_idle_id,
464 : : "[shumate] recompute_grid_in_idle_cb");
465 : : }
466 : :
467 : :
468 : : static void
469 : 0 : on_viewport_changed (ShumateMapLayer *self,
470 : : GParamSpec *pspec,
471 : : ShumateViewport *view)
472 : : {
473 [ # # ]: 0 : g_assert (SHUMATE_IS_MAP_LAYER (self));
474 : :
475 : 0 : queue_recompute_grid_in_idle (self);
476 : 0 : }
477 : :
478 : : static void
479 : 0 : shumate_map_layer_set_property (GObject *object,
480 : : guint property_id,
481 : : const GValue *value,
482 : : GParamSpec *pspec)
483 : : {
484 : 0 : ShumateMapLayer *self = SHUMATE_MAP_LAYER (object);
485 : :
486 [ # # ]: 0 : switch (property_id)
487 : : {
488 : 0 : case PROP_MAP_SOURCE:
489 : 0 : g_set_object (&self->map_source, g_value_get_object (value));
490 : 0 : break;
491 : :
492 : 0 : default:
493 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
494 : 0 : break;
495 : : }
496 : 0 : }
497 : :
498 : : static void
499 : 0 : shumate_map_layer_get_property (GObject *object,
500 : : guint property_id,
501 : : GValue *value,
502 : : GParamSpec *pspec)
503 : : {
504 : 0 : ShumateMapLayer *self = SHUMATE_MAP_LAYER (object);
505 : :
506 [ # # ]: 0 : switch (property_id)
507 : : {
508 : 0 : case PROP_MAP_SOURCE:
509 : 0 : g_value_set_object (value, self->map_source);
510 : 0 : break;
511 : :
512 : 0 : default:
513 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
514 : 0 : break;
515 : : }
516 : 0 : }
517 : :
518 : : static void
519 : 0 : shumate_map_layer_dispose (GObject *object)
520 : : {
521 : 0 : ShumateMapLayer *self = SHUMATE_MAP_LAYER (object);
522 : 0 : ShumateViewport *viewport = shumate_layer_get_viewport (SHUMATE_LAYER (self));
523 : 0 : GtkWidget *child;
524 : :
525 : 0 : g_signal_handlers_disconnect_by_data (viewport, self);
526 [ # # ]: 0 : while ((child = gtk_widget_get_first_child (GTK_WIDGET (object))))
527 : 0 : gtk_widget_unparent (child);
528 : :
529 [ # # ]: 0 : g_clear_handle_id (&self->recompute_grid_idle_id, g_source_remove);
530 [ # # ]: 0 : g_clear_pointer (&self->tile_fill, g_hash_table_unref);
531 [ # # ]: 0 : g_clear_pointer (&self->tile_children, g_hash_table_unref);
532 [ # # ]: 0 : g_clear_object (&self->map_source);
533 [ # # ]: 0 : g_clear_object (&self->memcache);
534 : :
535 : 0 : G_OBJECT_CLASS (shumate_map_layer_parent_class)->dispose (object);
536 : 0 : }
537 : :
538 : : #ifdef SHUMATE_HAS_VECTOR_RENDERER
539 : : static void
540 : 0 : on_symbol_clicked (ShumateMapLayer *self,
541 : : ShumateSymbolEvent *event,
542 : : ShumateVectorSymbolContainer *container)
543 : : {
544 : 0 : g_signal_emit (self, signals[SYMBOL_CLICKED], 0, event);
545 : 0 : }
546 : : #endif
547 : :
548 : : static void
549 : 0 : shumate_map_layer_constructed (GObject *object)
550 : : {
551 : 0 : ShumateInspectorSettings *settings = shumate_inspector_settings_get_default ();
552 : 0 : ShumateMapLayer *self = SHUMATE_MAP_LAYER (object);
553 : 0 : ShumateViewport *viewport;
554 : :
555 : 0 : G_OBJECT_CLASS (shumate_map_layer_parent_class)->constructed (object);
556 : :
557 : 0 : viewport = shumate_layer_get_viewport (SHUMATE_LAYER (self));
558 : 0 : g_signal_connect_swapped (viewport, "notify", G_CALLBACK (on_viewport_changed), self);
559 : :
560 : 0 : g_signal_connect_object (settings, "notify::show-tile-bounds", G_CALLBACK (queue_recompute_grid_in_idle), self, G_CONNECT_SWAPPED);
561 : :
562 : : #ifdef SHUMATE_HAS_VECTOR_RENDERER
563 : 0 : self->symbols = shumate_vector_symbol_container_new (self->map_source, viewport);
564 : 0 : g_signal_connect_object (self->symbols, "symbol-clicked", G_CALLBACK (on_symbol_clicked), self, G_CONNECT_SWAPPED);
565 : 0 : gtk_widget_set_parent (GTK_WIDGET (self->symbols), GTK_WIDGET (self));
566 : : #endif
567 : 0 : }
568 : :
569 : : static void
570 : 0 : shumate_map_layer_size_allocate (GtkWidget *widget,
571 : : int width,
572 : : int height,
573 : : int baseline)
574 : : {
575 : 0 : ShumateMapLayer *self = SHUMATE_MAP_LAYER (widget);
576 : 0 : GtkAllocation child_allocation;
577 : :
578 : : #ifdef SHUMATE_HAS_VECTOR_RENDERER
579 : : /* gtk_widget_measure needs to be called during size_allocate, but we don't
580 : : * care about the result here--the symbol container always gets the same
581 : : * size as the map layer */
582 : 0 : gtk_widget_measure (GTK_WIDGET (self->symbols), GTK_ORIENTATION_VERTICAL, -1, NULL, NULL, NULL, NULL);
583 : :
584 : 0 : child_allocation.x = 0;
585 : 0 : child_allocation.y = 0;
586 : 0 : child_allocation.width = width;
587 : 0 : child_allocation.height = height;
588 : 0 : gtk_widget_size_allocate (GTK_WIDGET (self->symbols), &child_allocation, baseline);
589 : : #endif
590 : :
591 : : /* Make sure the tile grid is up to date */
592 : 0 : queue_recompute_grid_in_idle (self);
593 : 0 : }
594 : :
595 : : static void
596 : 0 : shumate_map_layer_measure (GtkWidget *widget,
597 : : GtkOrientation orientation,
598 : : int for_size,
599 : : int *minimum,
600 : : int *natural,
601 : : int *minimum_baseline,
602 : : int *natural_baseline)
603 : : {
604 [ # # ]: 0 : if (minimum)
605 : 0 : *minimum = 0;
606 : :
607 [ # # ]: 0 : if (natural)
608 : 0 : *natural = 0;
609 : 0 : }
610 : :
611 : : static double
612 : 0 : snap_coordinate (double point,
613 : : double translate,
614 : : double size)
615 : : {
616 : 0 : return round ((point - translate) / size) * size + translate;
617 : : }
618 : :
619 : : static double
620 : 0 : round_px (double x, double scale_factor)
621 : : {
622 : 0 : return round (x * scale_factor) / scale_factor;
623 : : }
624 : :
625 : : static void
626 : 0 : shumate_map_layer_snapshot (GtkWidget *widget, GtkSnapshot *snapshot)
627 : : {
628 : 0 : ShumateMapLayer *self = SHUMATE_MAP_LAYER (widget);
629 : 0 : ShumateViewport *viewport = shumate_layer_get_viewport (SHUMATE_LAYER (self));
630 : 0 : double zoom_level = get_effective_zoom_level (self);
631 : 0 : int width = gtk_widget_get_width (GTK_WIDGET (self));
632 : 0 : int height = gtk_widget_get_height (GTK_WIDGET (self));
633 : 0 : double rotation = shumate_viewport_get_rotation (viewport);
634 : 0 : double latitude = shumate_location_get_latitude (SHUMATE_LOCATION (viewport));
635 : 0 : double longitude = shumate_location_get_longitude (SHUMATE_LOCATION (viewport));
636 : 0 : double latitude_y = shumate_map_source_get_y (self->map_source, zoom_level, latitude);
637 : 0 : double longitude_x = shumate_map_source_get_x (self->map_source, zoom_level, longitude);
638 : 0 : int tile_size = shumate_map_source_get_tile_size (self->map_source);
639 : 0 : double tile_size_for_zoom = shumate_map_source_get_tile_size_at_zoom (self->map_source, zoom_level);
640 : 0 : double map_width = shumate_map_source_get_column_count (self->map_source, zoom_level)
641 : 0 : * tile_size_for_zoom;
642 : 0 : double map_height = shumate_map_source_get_row_count (self->map_source, zoom_level)
643 : 0 : * tile_size_for_zoom;
644 : 0 : gboolean show_tile_bounds = shumate_inspector_settings_get_show_tile_bounds (shumate_inspector_settings_get_default ());
645 : 0 : double scale_factor = gtk_widget_get_scale_factor (widget);
646 : :
647 : 0 : GHashTableIter iter;
648 : 0 : gpointer key;
649 : 0 : gpointer value;
650 : :
651 : : /* Because Earth is round [citation needed], cylindrical projections like
652 : : * Mercator wrap around at the antimeridian. Moving across the antimeridian
653 : : * is the same as teleporting across the world: at one frame the longitude
654 : : * is just less than 180, and the next it's just more than -180.
655 : : *
656 : : * ShumateMapLayer doesn't handle teleportation well. Widgets can only be
657 : : * added/removed between frames, but animations are calculated during the
658 : : * frame. This means that by the time we know about the new viewport location,
659 : : * it's too late to move tiles around. recompute_grid(), which will fix the
660 : : * problem, won't be called until after the current frame.
661 : : *
662 : : * To fix this, recompute_grid() remembers the most recent location
663 : : * it saw. Then, to reduce "teleportation", here in snapshot() we render
664 : : * the "copy" of the new location that is closest to the one from
665 : : * recompute_grid(). This just means snapping the current location to a grid
666 : : * translated by the old location.
667 : : * */
668 : 0 : longitude_x = snap_coordinate (self->last_recompute_x * map_width, longitude_x, map_width);
669 : 0 : latitude_y = snap_coordinate (self->last_recompute_y * map_height, latitude_y, map_height);
670 : :
671 : : /* Scale and rotate around the center of the view */
672 : 0 : gtk_snapshot_save (snapshot);
673 : 0 : gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (width / 2.0, height / 2.0));
674 : 0 : gtk_snapshot_rotate (snapshot, rotation * 180 / G_PI);
675 : :
676 : 0 : gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (-width / 2.0, -height / 2.0));
677 : :
678 : 0 : g_hash_table_iter_init (&iter, self->tile_children);
679 : :
680 [ # # ]: 0 : while (g_hash_table_iter_next (&iter, &key, &value))
681 : : {
682 : 0 : ShumateGridPosition *pos = key;
683 : 0 : ShumateTile *tile = value;
684 : 0 : GdkPaintable *paintable = shumate_tile_get_paintable (tile);
685 : 0 : double size = tile_size * pow (2, zoom_level - pos->zoom);
686 : 0 : double x = -(longitude_x - width/2.0) + size * pos->x;
687 : 0 : double y = -(latitude_y - height/2.0) + size * pos->y;
688 : :
689 [ # # ]: 0 : if (paintable == NULL)
690 : 0 : continue;
691 : :
692 : 0 : gtk_snapshot_save (snapshot);
693 : :
694 : 0 : gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (
695 : : round_px (x, scale_factor),
696 : : round_px (y, scale_factor)
697 : : ));
698 : :
699 : 0 : gdk_paintable_snapshot (
700 : : paintable,
701 : : snapshot,
702 : 0 : round_px (x + size, scale_factor) - round_px (x, scale_factor),
703 : 0 : round_px (y + size, scale_factor) - round_px (y, scale_factor)
704 : : );
705 : :
706 [ # # ]: 0 : if (show_tile_bounds)
707 : : {
708 : 0 : g_autofree char *text = NULL;
709 : 0 : g_autoptr(PangoLayout) layout = NULL;
710 : 0 : int x = shumate_tile_get_x (tile);
711 : 0 : GdkRGBA color = {1, 0, 1, 1};
712 : :
713 [ # # ]: 0 : if (x == pos->x)
714 : 0 : text = g_strdup_printf (" %d, %d, %d", pos->zoom, pos->x, pos->y);
715 : : else
716 : 0 : text = g_strdup_printf (" %d, %d (%d), %d", pos->zoom, x, pos->x, pos->y);
717 : :
718 : 0 : layout = gtk_widget_create_pango_layout (widget, text);
719 : 0 : gtk_snapshot_append_layout (snapshot, layout, &color);
720 [ # # ]: 0 : gtk_snapshot_append_border (snapshot,
721 : 0 : &GSK_ROUNDED_RECT_INIT (0, 0, size, size),
722 : 0 : (float[]){1, 1, 1, 1},
723 : 0 : (GdkRGBA[]) { color, color, color, color });
724 : : }
725 : :
726 : 0 : gtk_snapshot_restore (snapshot);
727 : : }
728 : :
729 : 0 : gtk_snapshot_restore (snapshot);
730 : :
731 : : #ifdef SHUMATE_HAS_VECTOR_RENDERER
732 : 0 : gtk_widget_snapshot_child (widget, GTK_WIDGET (self->symbols), snapshot);
733 : : #endif
734 : 0 : }
735 : :
736 : : static char *
737 : 0 : shumate_map_layer_get_debug_text (ShumateLayer *layer)
738 : : {
739 : 0 : ShumateMapLayer *self = SHUMATE_MAP_LAYER (layer);
740 : 0 : g_autoptr(GString) string = g_string_new ("");
741 : 0 : int n_loading = 0;
742 : 0 : GHashTableIter iter;
743 : 0 : ShumateTile *tile;
744 : :
745 : 0 : g_hash_table_iter_init (&iter, self->tile_children);
746 [ # # ]: 0 : while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&tile))
747 : : {
748 [ # # ]: 0 : if (shumate_tile_get_state (tile) != SHUMATE_STATE_DONE)
749 : 0 : n_loading ++;
750 : : }
751 : :
752 : 0 : g_string_append_printf (string,
753 : : "tiles: %d, %d loading\n",
754 : : g_hash_table_size (self->tile_children),
755 : : n_loading);
756 : :
757 : : #ifdef SHUMATE_HAS_VECTOR_RENDERER
758 : : {
759 : 0 : g_autofree char *symbol_debug = shumate_vector_symbol_container_get_debug_text (self->symbols);
760 [ # # ]: 0 : g_string_append (string, symbol_debug);
761 : : }
762 : : #endif
763 : :
764 [ # # ]: 0 : if (self->deferring)
765 [ # # ]: 0 : g_string_append (string, "deferring\n");
766 : :
767 : 0 : return g_string_free_and_steal (g_steal_pointer (&string));
768 : : }
769 : :
770 : : static void
771 : 1 : shumate_map_layer_class_init (ShumateMapLayerClass *klass)
772 : : {
773 : 1 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
774 : 1 : GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
775 : 1 : ShumateLayerClass *layer_class = SHUMATE_LAYER_CLASS (klass);
776 : :
777 : 1 : object_class->set_property = shumate_map_layer_set_property;
778 : 1 : object_class->get_property = shumate_map_layer_get_property;
779 : 1 : object_class->dispose = shumate_map_layer_dispose;
780 : 1 : object_class->constructed = shumate_map_layer_constructed;
781 : :
782 : 1 : widget_class->size_allocate = shumate_map_layer_size_allocate;
783 : 1 : widget_class->snapshot = shumate_map_layer_snapshot;
784 : 1 : widget_class->measure = shumate_map_layer_measure;
785 : :
786 : 1 : layer_class->get_debug_text = shumate_map_layer_get_debug_text;
787 : :
788 : 2 : obj_properties[PROP_MAP_SOURCE] =
789 : 1 : g_param_spec_object ("map-source",
790 : : "Map Source",
791 : : "The Map Source",
792 : : SHUMATE_TYPE_MAP_SOURCE,
793 : : G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
794 : :
795 : 1 : g_object_class_install_properties (object_class,
796 : : N_PROPERTIES,
797 : : obj_properties);
798 : :
799 : : /**
800 : : * ShumateMapLayer::symbol-clicked:
801 : : * @self: the [class@MapLayer] emitting the signal
802 : : * @event: a [class@SymbolEvent] with details about the clicked symbol.
803 : : *
804 : : * Emitted when a symbol in the map layer is clicked.
805 : : *
806 : : * Since: 1.1
807 : : */
808 : 2 : signals[SYMBOL_CLICKED] =
809 : 1 : g_signal_new ("symbol-clicked",
810 : : G_OBJECT_CLASS_TYPE (object_class),
811 : : G_SIGNAL_RUN_LAST,
812 : : 0, NULL, NULL,
813 : : g_cclosure_marshal_VOID__OBJECT,
814 : : G_TYPE_NONE,
815 : : 1,
816 : : SHUMATE_TYPE_SYMBOL_EVENT);
817 : 1 : }
818 : :
819 : : static void
820 : 0 : shumate_map_layer_init (ShumateMapLayer *self)
821 : : {
822 : 0 : self->tile_children = g_hash_table_new_full (shumate_grid_position_hash, shumate_grid_position_equal, shumate_grid_position_free, g_object_unref);
823 : 0 : self->tile_fill = g_hash_table_new_full (g_direct_hash, g_direct_equal, g_object_unref, g_object_unref);
824 : 0 : self->memcache = shumate_memory_cache_new_full (100);
825 : 0 : }
826 : :
827 : : ShumateMapLayer *
828 : 0 : shumate_map_layer_new (ShumateMapSource *map_source,
829 : : ShumateViewport *viewport)
830 : : {
831 : 0 : return g_object_new (SHUMATE_TYPE_MAP_LAYER,
832 : : "map-source", map_source,
833 : : "viewport", viewport,
834 : : NULL);
835 : : }
|