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