GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 96.8% 122 / 0 / 126
Functions: 100.0% 12 / 0 / 12
Branches: 71.3% 77 / 0 / 108

shumate/shumate-vector-sprite-sheet.c
Line Branch Exec Source
1 /*
2 * Copyright (C) 2022 James Westman <james@jwestman.net>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library; if not, see <https://www.gnu.org/licenses/>.
16 */
17
18 #include <json-glib/json-glib.h>
19 #include "shumate-vector-sprite-sheet.h"
20 #include "shumate-vector-renderer.h"
21 #include "vector/shumate-vector-utils-private.h"
22
23 /**
24 * ShumateVectorSpriteSheet:
25 *
26 * A collection of [class@VectorSprite]s.
27 *
28 * Sprites are used as icons in symbols or as the pattern for a fill layer.
29 *
30 * Most MapLibre stylesheets provide their spritesheet as a PNG image and a JSON
31 * description of the sprites. This spritesheet can be added using
32 * [method@VectorSpriteSheet.add_page]. Sprites can also be added individually
33 * using [method@VectorSpriteSheet.add_sprite].
34 *
35 * Some map styles rely on application code to provide some or all of their
36 * sprites. This is supported using a fallback function, which can be set using
37 * [method@VectorSpriteSheet.set_fallback]. This function can generate sprites
38 * on demand. For example, it could load a symbolic icon from the [class@Gtk.IconTheme]
39 * or render a custom highway shield.
40 *
41 * ## HiDPI support
42 *
43 * Map styles should provide a double resolution spritesheet for high DPI
44 * displays. That spritesheet can be added as a separate page.
45 * The [class@VectorSpriteSheet] will pick the best sprites for the display's
46 * scale factor.
47 *
48 * If a fallback function is set, it receives the requested scale factor
49 * as an argument. It should use this to generate the sprite at the correct size.
50 * For example, if the scale factor is 2, the image should be twice as large
51 * (but the *sprite's* width and height should be the same).
52 *
53 * ## Thread Safety
54 *
55 * [class@VectorSpriteSheet] is thread safe.
56 *
57 * Since: 1.1
58 */
59
60 /**
61 * ShumateVectorSpriteFallbackFunc:
62 * @sprite_sheet: the [class@VectorSpriteSheet]
63 * @name: the name of the sprite to generate
64 * @scale: the scale factor of the sprite
65 * @user_data: user data passed to [method@VectorSpriteSheet.set_fallback]
66 *
67 * A function to generate sprites for a [class@VectorSpriteSheet].
68 *
69 * See [method@VectorSpriteSheet.set_fallback].
70 *
71 * Returns: (transfer full) (nullable): a [class@VectorSprite] or %NULL
72 *
73 * Since: 1.1
74 */
75
76 #define FALLBACK_QUEUE_CAPACITY 100
77
78 struct _ShumateVectorSpriteSheet
79 {
80 GObject parent_instance;
81
82 GRecMutex mutex;
83
84 GHashTable *sprite_arrays;
85
86 ShumateVectorSpriteFallbackFunc *fallback;
87 gpointer fallback_user_data;
88 GDestroyNotify fallback_destroy;
89 GHashTable *fallback_sprites;
90 GQueue *fallback_queue;
91 };
92
93
4/6
shumate_vector_sprite_sheet_class_intern_init:
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 7 times.
shumate_vector_sprite_sheet_get_type:
✓ Branch 2 → 3 taken 9 times.
✓ Branch 2 → 7 taken 467 times.
✓ Branch 4 → 5 taken 9 times.
✗ Branch 4 → 7 not taken.
492 G_DEFINE_FINAL_TYPE (ShumateVectorSpriteSheet, shumate_vector_sprite_sheet, G_TYPE_OBJECT)
94
95
96 /**
97 * shumate_vector_sprite_sheet_new:
98 *
99 * Creates a new, empty [class@VectorSpriteSheet].
100 *
101 * Returns: (transfer full): a new [class@VectorSpriteSheet]
102 *
103 * Since: 1.1
104 */
105 ShumateVectorSpriteSheet *
106 10 shumate_vector_sprite_sheet_new (void)
107 {
108 10 return g_object_new (SHUMATE_TYPE_VECTOR_SPRITE_SHEET, NULL);
109 }
110
111 static void
112 10 shumate_vector_sprite_sheet_finalize (GObject *object)
113 {
114 10 ShumateVectorSpriteSheet *self = (ShumateVectorSpriteSheet *)object;
115
116
1/2
✓ Branch 2 → 3 taken 10 times.
✗ Branch 2 → 4 not taken.
10 g_clear_pointer (&self->sprite_arrays, g_hash_table_unref);
117
118
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 8 times.
10 if (self->fallback_destroy != NULL)
119 2 self->fallback_destroy (self->fallback_user_data);
120
2/2
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 8 taken 8 times.
10 g_clear_pointer (&self->fallback_sprites, g_hash_table_unref);
121
2/2
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 8 times.
10 if (self->fallback_queue != NULL)
122 2 g_queue_free_full (self->fallback_queue, g_free);
123
124 10 g_rec_mutex_clear (&self->mutex);
125
126 10 G_OBJECT_CLASS (shumate_vector_sprite_sheet_parent_class)->finalize (object);
127 10 }
128
129 static void
130 7 shumate_vector_sprite_sheet_class_init (ShumateVectorSpriteSheetClass *klass)
131 {
132 7 GObjectClass *object_class = G_OBJECT_CLASS (klass);
133
134 7 object_class->finalize = shumate_vector_sprite_sheet_finalize;
135 }
136
137 static void
138 10 shumate_vector_sprite_sheet_init (ShumateVectorSpriteSheet *self)
139 {
140 10 g_rec_mutex_init (&self->mutex);
141 10 self->sprite_arrays = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_ptr_array_unref);
142 10 }
143
144 /**
145 * shumate_vector_sprite_sheet_add_sprite:
146 * @self: a [class@VectorSpriteSheet]
147 * @name: the name of the sprite
148 * @sprite: a [class@VectorSprite]
149 *
150 * Adds a sprite to the spritesheet.
151 *
152 * Since: 1.1
153 */
154 void
155 10 shumate_vector_sprite_sheet_add_sprite (ShumateVectorSpriteSheet *self,
156 const char *name,
157 ShumateVectorSprite *sprite)
158 {
159 20 g_autoptr(GRecMutexLocker) locker = NULL;
160 10 GPtrArray *array = NULL;
161
162
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 10 times.
10 g_return_if_fail (SHUMATE_IS_VECTOR_SPRITE_SHEET (self));
163
1/2
✓ Branch 5 → 6 taken 10 times.
✗ Branch 5 → 8 not taken.
10 g_return_if_fail (name != NULL);
164
1/2
✓ Branch 7 → 9 taken 10 times.
✗ Branch 7 → 12 not taken.
10 g_return_if_fail (SHUMATE_IS_VECTOR_SPRITE (sprite));
165
166 10 locker = g_rec_mutex_locker_new (&self->mutex);
167
168 10 array = g_hash_table_lookup (self->sprite_arrays, name);
169
2/2
✓ Branch 11 → 13 taken 8 times.
✓ Branch 11 → 19 taken 2 times.
10 if (array == NULL)
170 {
171 8 array = g_ptr_array_new_with_free_func (g_object_unref);
172
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 17 taken 8 times.
16 g_hash_table_insert (self->sprite_arrays, g_strdup (name), array);
173 }
174
175 10 g_ptr_array_add (array, g_object_ref (sprite));
176 }
177
178 /**
179 * shumate_vector_sprite_sheet_add_page:
180 * @self: a [class@VectorSpriteSheet]
181 * @texture: a [class@Gdk.Texture]
182 * @json: a JSON string
183 * @default_scale: the default scale factor of the page
184 * @error: return location for a [struct@GLib.Error], or %NULL
185 *
186 * Adds a page to the spritesheet.
187 *
188 * See <https://maplibre.org/maplibre-gl-js-docs/style-spec/sprite/> for
189 * details about the spritesheet format. Most stylesheets provide these files
190 * along with the main style JSON.
191 *
192 * Map styles should provide a double resolution spritesheet for high DPI
193 * displays. That spritesheet should be added as its own page, with a
194 * @default_scale of 2.
195 *
196 * Returns: %TRUE if the page was added successfully, %FALSE otherwise
197 *
198 * Since: 1.1
199 */
200 gboolean
201 10 shumate_vector_sprite_sheet_add_page (ShumateVectorSpriteSheet *self,
202 GdkTexture *texture,
203 const char *json,
204 double default_scale,
205 GError **error)
206 {
207
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 10 times.
10 g_return_val_if_fail (SHUMATE_IS_VECTOR_SPRITE_SHEET (self), FALSE);
208
4/8
✓ Branch 5 → 6 taken 10 times.
✗ Branch 5 → 10 not taken.
✓ Branch 6 → 7 taken 10 times.
✗ Branch 6 → 8 not taken.
✓ Branch 7 → 8 taken 10 times.
✗ Branch 7 → 9 not taken.
✓ Branch 8 → 9 taken 10 times.
✗ Branch 8 → 10 not taken.
10 g_return_val_if_fail (GDK_IS_TEXTURE (texture), FALSE);
209
1/2
✓ Branch 9 → 11 taken 10 times.
✗ Branch 9 → 13 not taken.
10 g_return_val_if_fail (json != NULL, FALSE);
210
211 /* No mutex lock is needed for this function because it only references @self
212 via shumate_vector_sprite_sheet_add_sprite(), which has its own lock. */
213
214 20 g_autoptr(JsonNode) json_node = NULL;
215 10 JsonObject *sprites;
216 10 JsonObjectIter iter;
217 10 const char *sprite_name;
218 10 JsonNode *sprite_node;
219
220 10 json_node = json_from_string (json, error);
221
1/2
✓ Branch 12 → 14 taken 10 times.
✗ Branch 12 → 39 not taken.
10 if (json_node == NULL)
222 return FALSE;
223
224
1/2
✓ Branch 15 → 16 taken 10 times.
✗ Branch 15 → 38 not taken.
10 if (!shumate_vector_json_get_object (json_node, &sprites, error))
225 return FALSE;
226
227 10 json_object_iter_init (&iter, sprites);
228
2/2
✓ Branch 37 → 17 taken 10 times.
✓ Branch 37 → 38 taken 10 times.
20 while (json_object_iter_next (&iter, &sprite_name, &sprite_node))
229 {
230 10 JsonObject *sprite_object;
231 10 g_autoptr(ShumateVectorSprite) sprite = NULL;
232 10 int x, y, width, height, pixel_ratio;
233
234
1/2
✓ Branch 18 → 19 taken 10 times.
✗ Branch 18 → 34 not taken.
10 if (!shumate_vector_json_get_object (sprite_node, &sprite_object, error))
235 return FALSE;
236
237 10 x = json_object_get_int_member_with_default (sprite_object, "x", 0);
238 10 y = json_object_get_int_member_with_default (sprite_object, "y", 0);
239 10 width = json_object_get_int_member_with_default (sprite_object, "width", 0);
240 10 height = json_object_get_int_member_with_default (sprite_object, "height", 0);
241 10 pixel_ratio = json_object_get_int_member_with_default (sprite_object, "pixelRatio", default_scale);
242
243
2/4
✓ Branch 24 → 25 taken 10 times.
✗ Branch 24 → 26 not taken.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 28 taken 10 times.
10 if (x < 0 || y < 0 || width <= 0 || height <= 0)
244 {
245 ✗ g_set_error (error,
246 SHUMATE_STYLE_ERROR,
247 SHUMATE_STYLE_ERROR_MALFORMED_STYLE,
248 "Invalid dimensions for sprite '%s'", sprite_name);
249 ✗ return FALSE;
250 }
251
252 20 sprite = shumate_vector_sprite_new_full (
253 GDK_PAINTABLE (texture),
254 width / pixel_ratio,
255 height / pixel_ratio,
256 pixel_ratio,
257 10 &(GdkRectangle){ x, y, width, height }
258 );
259
260
1/2
✓ Branch 30 → 31 taken 10 times.
✗ Branch 30 → 32 not taken.
10 shumate_vector_sprite_sheet_add_sprite (self, sprite_name, sprite);
261 }
262
263 return TRUE;
264 }
265
266 static ShumateVectorSprite *
267 228 search_sprites (GPtrArray *sprites,
268 double scale,
269 gboolean higher,
270 gboolean lower)
271 {
272
2/2
✓ Branch 2 → 3 taken 224 times.
✓ Branch 2 → 4 taken 4 times.
228 double best_scale = higher ? G_MAXDOUBLE : G_MINDOUBLE;
273 228 ShumateVectorSprite *best_sprite = NULL;
274
275
2/2
✓ Branch 16 → 5 taken 240 times.
✓ Branch 16 → 17 taken 10 times.
250 for (int i = 0; i < sprites->len; i++)
276 {
277 240 ShumateVectorSprite *sprite = g_ptr_array_index (sprites, i);
278 240 double sprite_scale = shumate_vector_sprite_get_scale_factor (sprite);
279
280
2/2
✓ Branch 6 → 7 taken 8 times.
✓ Branch 6 → 10 taken 232 times.
240 if (sprite_scale > scale)
281 {
282
4/4
✓ Branch 7 → 8 taken 4 times.
✓ Branch 7 → 9 taken 4 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 15 taken 2 times.
8 if (higher && sprite_scale < best_scale)
283 {
284 best_scale = sprite_scale;
285 best_sprite = sprite;
286 }
287 else
288 6 continue;
289 }
290
2/2
✓ Branch 10 → 11 taken 14 times.
✓ Branch 10 → 14 taken 218 times.
232 else if (sprite_scale < scale)
291 {
292
3/4
✓ Branch 11 → 12 taken 4 times.
✓ Branch 11 → 13 taken 10 times.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 15 taken 4 times.
14 if (lower && sprite_scale > best_scale)
293 {
294 best_scale = sprite_scale;
295 best_sprite = sprite;
296 }
297 else
298 10 continue;
299 }
300 else
301 218 return g_object_ref (sprite);
302 }
303
304
2/2
✓ Branch 17 → 18 taken 4 times.
✓ Branch 17 → 19 taken 6 times.
10 if (best_sprite != NULL)
305 4 return g_object_ref (best_sprite);
306 else
307 return NULL;
308 }
309
310
311 /**
312 * shumate_vector_sprite_sheet_get_sprite:
313 * @self: a [class@VectorSpriteSheet]
314 * @name: an icon name
315 * @scale: the scale factor of the icon
316 *
317 * Gets a sprite from the spritesheet.
318 *
319 * The returned sprite might not be at the requested scale factor if an exact
320 * match is not found.
321 *
322 * Returns: (transfer full) (nullable): a [class@VectorSprite], or %NULL if the
323 * icon does not exist.
324 *
325 * Since: 1.1
326 */
327 ShumateVectorSprite *
328 438 shumate_vector_sprite_sheet_get_sprite (ShumateVectorSpriteSheet *self,
329 const char *name,
330 double scale)
331 {
332 876 g_autoptr(GRecMutexLocker) locker = NULL;
333 438 ShumateVectorSprite *sprite;
334 438 GPtrArray *sprite_array;
335
336
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 438 times.
438 g_return_val_if_fail (SHUMATE_IS_VECTOR_SPRITE_SHEET (self), NULL);
337
1/2
✓ Branch 5 → 6 taken 438 times.
✗ Branch 5 → 9 not taken.
438 g_return_val_if_fail (name != NULL, NULL);
338
339 438 locker = g_rec_mutex_locker_new (&self->mutex);
340
341 438 sprite_array = g_hash_table_lookup (self->sprite_arrays, name);
342
2/2
✓ Branch 8 → 10 taken 222 times.
✓ Branch 8 → 16 taken 216 times.
438 if (sprite_array != NULL)
343 {
344 /* Search the exact scale, then higher scales, then lower scales */
345
2/2
✓ Branch 11 → 12 taken 4 times.
✓ Branch 11 → 40 taken 218 times.
222 if ((sprite = search_sprites (sprite_array, scale, FALSE, FALSE)) != NULL)
346 return sprite;
347
2/2
✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 40 taken 2 times.
4 if ((sprite = search_sprites (sprite_array, scale, TRUE, FALSE)) != NULL)
348 return sprite;
349
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 40 taken 2 times.
2 if ((sprite = search_sprites (sprite_array, scale, FALSE, TRUE)) != NULL)
350 return sprite;
351 }
352
353
2/2
✓ Branch 16 → 17 taken 212 times.
✓ Branch 16 → 40 taken 4 times.
216 if (self->fallback)
354 {
355
2/2
✓ Branch 18 → 19 taken 4 times.
✓ Branch 18 → 21 taken 208 times.
212 if (g_hash_table_lookup_extended (self->fallback_sprites, name, NULL, (gpointer *)&sprite))
356 {
357
2/2
✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 40 taken 2 times.
4 if (sprite != NULL)
358 2 return g_object_ref (sprite);
359 else
360 return NULL;
361 }
362
363 208 sprite = self->fallback (self, name, scale, self->fallback_user_data);
364
365
1/2
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 25 taken 208 times.
416 g_hash_table_insert (self->fallback_sprites, g_strdup (name), sprite);
366
1/2
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 30 taken 208 times.
416 g_queue_push_tail (self->fallback_queue, g_strdup (name));
367
2/2
✓ Branch 33 → 34 taken 8 times.
✓ Branch 33 → 38 taken 200 times.
208 if (g_queue_get_length (self->fallback_queue) > FALLBACK_QUEUE_CAPACITY)
368 {
369 16 g_autofree char *old_name = g_queue_pop_head (self->fallback_queue);
370 8 g_hash_table_remove (self->fallback_sprites, old_name);
371 }
372
373
2/2
✓ Branch 38 → 39 taken 206 times.
✓ Branch 38 → 40 taken 2 times.
208 if (sprite != NULL)
374 206 return g_object_ref (sprite);
375 }
376
377 return NULL;
378 }
379
380
381 static void
382 208 object_unref0 (gpointer object)
383 {
384
2/2
✓ Branch 2 → 3 taken 206 times.
✓ Branch 2 → 4 taken 2 times.
208 if (object != NULL)
385 206 g_object_unref (object);
386 208 }
387
388
389 /**
390 * shumate_vector_sprite_sheet_set_fallback:
391 * @self: a [class@VectorSpriteSheet]
392 * @fallback: (nullable): a [callback@ShumateVectorSpriteFallbackFunc] or %NULL
393 * @user_data: user data to pass to @fallback
394 * @notify: a [callback@GLib.DestroyNotify] for @user_data
395 *
396 * Sets a fallback function to generate sprites.
397 *
398 * The fallback function is called when a texture is not found in the sprite
399 * sheet. It receives the icon name and scale factor, and should return a
400 * [class@VectorSprite], or %NULL if the icon could not be generated.
401 * It may be called in a different thread, and it may be called multiple times
402 * for the same icon name.
403 *
404 * If a previous fallback function was set, it will be replaced and any sprites
405 * it generated will be cleared.
406 *
407 * @fallback may be %NULL to clear the fallback function.
408 *
409 * Since: 1.1
410 */
411 void
412 2 shumate_vector_sprite_sheet_set_fallback (ShumateVectorSpriteSheet *self,
413 ShumateVectorSpriteFallbackFunc fallback,
414 gpointer user_data,
415 GDestroyNotify notify)
416 {
417 4 g_autoptr(GRecMutexLocker) locker = NULL;
418
419
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
2 g_return_if_fail (SHUMATE_IS_VECTOR_SPRITE_SHEET (self));
420
1/2
✓ Branch 5 → 6 taken 2 times.
✗ Branch 5 → 8 not taken.
2 g_return_if_fail (!(fallback == NULL && user_data != NULL));
421
422 2 locker = g_rec_mutex_locker_new (&self->mutex);
423
424 /* Clear any previous fallback func */
425
1/2
✗ Branch 7 → 9 not taken.
✓ Branch 7 → 10 taken 2 times.
2 if (self->fallback_destroy != NULL)
426 ✗ self->fallback_destroy (self->fallback_user_data);
427
428 2 self->fallback = NULL;
429 2 self->fallback_user_data = NULL;
430 2 self->fallback_destroy = NULL;
431
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 2 times.
2 g_clear_pointer (&self->fallback_sprites, g_hash_table_unref);
432
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 2 times.
2 if (self->fallback_queue != NULL)
433 ✗ g_queue_free_full (self->fallback_queue, g_free);
434
435
1/2
✓ Branch 14 → 15 taken 2 times.
✗ Branch 14 → 18 not taken.
2 if (fallback != NULL)
436 {
437 2 self->fallback = fallback;
438 2 self->fallback_user_data = user_data;
439 2 self->fallback_destroy = notify;
440
441 2 self->fallback_sprites = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, object_unref0);
442 2 self->fallback_queue = g_queue_new ();
443 }
444 }
445