[libshumate] vector: Add symbol layer



commit a2b1cc14f682ae5408329bf4cfb56213a0a7249a
Author: James Westman <james jwestman net>
Date:   Fri Jan 7 00:38:25 2022 -0600

    vector: Add symbol layer

 demos/map-style.json                               |  11 +
 shumate/meson.build                                |   9 +
 shumate/shumate-map-layer.c                        | 101 ++++++-
 shumate/shumate-memory-cache.c                     |  28 +-
 shumate/shumate-memory-cache.h                     |   7 +-
 shumate/shumate-tile-private.h                     |  25 ++
 shumate/shumate-tile.c                             |  26 ++
 shumate/shumate-vector-renderer.c                  |  28 +-
 shumate/vector/shumate-vector-expression.c         |   8 +-
 shumate/vector/shumate-vector-layer.c              |   3 +
 .../vector/shumate-vector-render-scope-private.h   |   9 +
 shumate/vector/shumate-vector-render-scope.c       |  64 +++++
 .../shumate-vector-symbol-container-private.h      |  43 +++
 shumate/vector/shumate-vector-symbol-container.c   | 318 +++++++++++++++++++++
 .../vector/shumate-vector-symbol-info-private.h    |  60 ++++
 shumate/vector/shumate-vector-symbol-info.c        |  85 ++++++
 .../vector/shumate-vector-symbol-layer-private.h   |  31 ++
 shumate/vector/shumate-vector-symbol-layer.c       | 151 ++++++++++
 shumate/vector/shumate-vector-symbol-private.h     |  32 +++
 shumate/vector/shumate-vector-symbol.c             | 177 ++++++++++++
 tests/memory-cache.c                               |  38 +--
 21 files changed, 1196 insertions(+), 58 deletions(-)
---
diff --git a/demos/map-style.json b/demos/map-style.json
index 76a09dc..08ef218 100644
--- a/demos/map-style.json
+++ b/demos/map-style.json
@@ -53,6 +53,17 @@
         "line-opacity": 0.5,
         "line-width": 0.9
       }
+    },
+    {
+      "id": "cities",
+      "type": "symbol",
+      "source-layer": "city",
+      "layout": {
+        "text-field": "{name}"
+      },
+      "paint": {
+        "text-color": "#000000"
+      }
     }
   ]
 }
diff --git a/shumate/meson.build b/shumate/meson.build
index 7f387fd..c39e64e 100644
--- a/shumate/meson.build
+++ b/shumate/meson.build
@@ -29,6 +29,7 @@ libshumate_public_h = [
 libshumate_private_h = [
   'shumate-kinetic-scrolling-private.h',
   'shumate-marker-private.h',
+  'shumate-tile-private.h',
   'shumate-viewport-private.h',
 
   'vector/shumate-vector-background-layer-private.h',
@@ -41,6 +42,10 @@ libshumate_private_h = [
   'vector/shumate-vector-layer-private.h',
   'vector/shumate-vector-line-layer-private.h',
   'vector/shumate-vector-render-scope-private.h',
+  'vector/shumate-vector-symbol-private.h',
+  'vector/shumate-vector-symbol-container-private.h',
+  'vector/shumate-vector-symbol-info-private.h',
+  'vector/shumate-vector-symbol-layer-private.h',
   'vector/shumate-vector-utils-private.h',
   'vector/shumate-vector-value-private.h',
   'vector/vector_tile.pb-c.h',
@@ -163,6 +168,10 @@ if get_option('vector_renderer')
     'vector/shumate-vector-layer.c',
     'vector/shumate-vector-line-layer.c',
     'vector/shumate-vector-render-scope.c',
+    'vector/shumate-vector-symbol.c',
+    'vector/shumate-vector-symbol-container.c',
+    'vector/shumate-vector-symbol-info.c',
+    'vector/shumate-vector-symbol-layer.c',
     'vector/shumate-vector-utils.c',
     'vector/shumate-vector-value.c',
     'vector/vector_tile.pb-c.c',
diff --git a/shumate/shumate-map-layer.c b/shumate/shumate-map-layer.c
index 800105d..4b581c7 100644
--- a/shumate/shumate-map-layer.c
+++ b/shumate/shumate-map-layer.c
@@ -19,6 +19,11 @@
 
 #include "shumate-map-layer.h"
 #include "shumate-memory-cache.h"
+#include "shumate-tile-private.h"
+
+#ifdef SHUMATE_HAS_VECTOR_RENDERER
+#  include "vector/shumate-vector-symbol-container-private.h"
+#endif
 
 /**
  * ShumateMapLayer:
@@ -43,6 +48,10 @@ struct _ShumateMapLayer
   guint recompute_grid_idle_id;
 
   ShumateMemoryCache *memcache;
+
+#ifdef SHUMATE_HAS_VECTOR_RENDERER
+  ShumateVectorSymbolContainer *symbols;
+#endif
 };
 
 G_DEFINE_TYPE (ShumateMapLayer, shumate_map_layer, SHUMATE_TYPE_LAYER)
@@ -127,6 +136,7 @@ typedef struct {
   ShumateMapLayer *self;
   ShumateTile *tile;
   char *source_id;
+  TileGridPosition pos;
 } TileFilledData;
 
 static void
@@ -139,8 +149,31 @@ tile_filled_data_free (TileFilledData *data)
 }
 G_DEFINE_AUTOPTR_CLEANUP_FUNC (TileFilledData, tile_filled_data_free);
 
+
+static void
+add_symbols (ShumateMapLayer  *self,
+             ShumateTile      *tile,
+             TileGridPosition *pos)
+{
+#ifdef SHUMATE_HAS_VECTOR_RENDERER
+  GPtrArray *symbols;
+
+  g_assert (SHUMATE_IS_MAP_LAYER (self));
+  g_assert (SHUMATE_IS_TILE (tile));
+
+  if ((symbols = shumate_tile_get_symbols (tile)))
+    shumate_vector_symbol_container_add_symbols (self->symbols,
+                                                 symbols,
+                                                 pos->x,
+                                                 pos->y,
+                                                 pos->zoom);
+#endif
+}
+
 static void
-on_tile_filled (GObject *source_object, GAsyncResult *res, gpointer user_data)
+on_tile_filled (GObject      *source_object,
+                GAsyncResult *res,
+                gpointer      user_data)
 {
   g_autoptr(TileFilledData) data = user_data;
   g_autoptr(GError) error = NULL;
@@ -152,25 +185,32 @@ on_tile_filled (GObject *source_object, GAsyncResult *res, gpointer user_data)
   if (!success)
     return;
 
-  shumate_memory_cache_store_texture (data->self->memcache,
-                                      data->tile,
-                                      shumate_tile_get_texture (data->tile),
-                                      data->source_id);
+  add_symbols (data->self, data->tile, &data->pos);
+
+  shumate_memory_cache_store_tile (data->self->memcache,
+                                   data->tile,
+                                   data->source_id);
 }
 
 static void
-add_tile (ShumateMapLayer *self,
-          ShumateTile     *tile)
+add_tile (ShumateMapLayer  *self,
+          ShumateTile      *tile,
+          TileGridPosition *pos)
 {
   const char *source_id = shumate_map_source_get_id (self->map_source);
 
-  if (!shumate_memory_cache_try_fill_tile (self->memcache, tile, source_id))
+  if (shumate_memory_cache_try_fill_tile (self->memcache, tile, source_id))
+    {
+      add_symbols (self, tile, pos);
+    }
+  else
     {
       GCancellable *cancellable = g_cancellable_new ();
       TileFilledData *data = g_new0 (TileFilledData, 1);
       data->self = g_object_ref (self);
       data->tile = g_object_ref (tile);
       data->source_id = g_strdup (source_id);
+      data->pos = *pos;
 
       shumate_tile_set_texture (tile, NULL);
       shumate_map_source_fill_tile_async (self->map_source, tile, cancellable, on_tile_filled, data);
@@ -178,11 +218,13 @@ add_tile (ShumateMapLayer *self,
     }
 
   gtk_widget_insert_before (GTK_WIDGET (tile), GTK_WIDGET (self), NULL);
+  g_hash_table_insert (self->tile_children, pos, g_object_ref (tile));
 }
 
 static void
-remove_tile (ShumateMapLayer *self,
-             ShumateTile     *tile)
+remove_tile (ShumateMapLayer  *self,
+             ShumateTile      *tile,
+             TileGridPosition *pos)
 {
   GCancellable *cancellable = g_hash_table_lookup (self->tile_fill, tile);
   if (cancellable)
@@ -191,6 +233,10 @@ remove_tile (ShumateMapLayer *self,
       g_hash_table_remove (self->tile_fill, tile);
     }
 
+#ifdef SHUMATE_HAS_VECTOR_RENDERER
+  shumate_vector_symbol_container_remove_symbols (self->symbols, pos->x, pos->y, pos->zoom);
+#endif
+
   gtk_widget_unparent (GTK_WIDGET (tile));
 }
 
@@ -251,7 +297,7 @@ recompute_grid (ShumateMapLayer *self)
           || pos->y >= tile_initial_row + required_rows)
           && pos->zoom == zoom_level)
         {
-          remove_tile (self, tile);
+          remove_tile (self, tile, pos);
           g_hash_table_iter_remove (&iter);
         }
     }
@@ -268,8 +314,7 @@ recompute_grid (ShumateMapLayer *self)
           if (!tile)
             {
               tile = shumate_tile_new_full (positive_mod (x, source_columns), positive_mod (y, source_rows), 
tile_size, zoom_level);
-              g_hash_table_insert (self->tile_children, g_steal_pointer (&pos), g_object_ref (tile));
-              add_tile (self, tile);
+              add_tile (self, tile, g_steal_pointer (&pos));
             }
 
           if (shumate_tile_get_state (tile) != SHUMATE_STATE_DONE)
@@ -289,7 +334,7 @@ recompute_grid (ShumateMapLayer *self)
 
           if (pos->zoom != zoom_level)
             {
-              remove_tile (self, tile);
+              remove_tile (self, tile, pos);
               g_hash_table_iter_remove (&iter);
             }
         }
@@ -444,6 +489,10 @@ shumate_map_layer_constructed (GObject *object)
   g_signal_connect_swapped (viewport, "notify::zoom-level", G_CALLBACK (on_view_zoom_level_changed), self);
   g_signal_connect_swapped (viewport, "notify::rotation", G_CALLBACK (on_view_rotation_changed), self);
 
+#ifdef SHUMATE_HAS_VECTOR_RENDERER
+  self->symbols = shumate_vector_symbol_container_new (self->map_source, viewport);
+  gtk_widget_set_parent (GTK_WIDGET (self->symbols), GTK_WIDGET (self));
+#endif
 }
 
 static void
@@ -484,6 +533,14 @@ shumate_map_layer_size_allocate (GtkWidget *widget,
       gtk_widget_size_allocate (GTK_WIDGET (tile), &child_allocation, baseline);
     }
 
+#ifdef SHUMATE_HAS_VECTOR_RENDERER
+  child_allocation.x = 0;
+  child_allocation.y = 0;
+  child_allocation.width = width;
+  child_allocation.height = height;
+  gtk_widget_size_allocate (GTK_WIDGET (self->symbols), &child_allocation, baseline);
+#endif
+
   /* We can't recompute while allocating, so queue an idle callback to run
    * the recomputation outside the allocation cycle.
    */
@@ -533,14 +590,28 @@ shumate_map_layer_snapshot (GtkWidget *widget, GtkSnapshot *snapshot)
   int width = gtk_widget_get_width (GTK_WIDGET (self));
   int height = gtk_widget_get_height (GTK_WIDGET (self));
   double rotation = shumate_viewport_get_rotation (viewport);
+  GtkWidget *child;
 
   /* Scale and rotate around the center of the view */
+  gtk_snapshot_save (snapshot);
   gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (width / 2.0, height / 2.0));
   gtk_snapshot_scale (snapshot, extra_zoom, extra_zoom);
   gtk_snapshot_rotate (snapshot, rotation * 180 / G_PI);
   gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (-width / 2.0, -height / 2.0));
 
-  GTK_WIDGET_CLASS (shumate_map_layer_parent_class)->snapshot (widget, snapshot);
+  for (child = gtk_widget_get_first_child (widget);
+       child != NULL;
+       child = gtk_widget_get_next_sibling (child))
+    {
+      if (SHUMATE_IS_TILE (child))
+        gtk_widget_snapshot_child (widget, child, snapshot);
+    }
+
+  gtk_snapshot_restore (snapshot);
+
+#ifdef SHUMATE_HAS_VECTOR_RENDERER
+  gtk_widget_snapshot_child (widget, GTK_WIDGET (self->symbols), snapshot);
+#endif
 }
 
 static void
diff --git a/shumate/shumate-memory-cache.c b/shumate/shumate-memory-cache.c
index af49816..d5b86ef 100644
--- a/shumate/shumate-memory-cache.c
+++ b/shumate/shumate-memory-cache.c
@@ -26,6 +26,7 @@
  */
 
 #include "shumate-memory-cache.h"
+#include "shumate-tile-private.h"
 
 #include <glib.h>
 #include <string.h>
@@ -49,6 +50,7 @@ typedef struct
 {
   char *key;
   GdkTexture *texture;
+  GPtrArray *symbols;
 } QueueMember;
 
 
@@ -145,8 +147,8 @@ shumate_memory_cache_new_full (guint size_limit)
   ShumateMemoryCache *cache;
 
   cache = g_object_new (SHUMATE_TYPE_MEMORY_CACHE,
-        "size-limit", size_limit,
-        NULL);
+                        "size-limit", size_limit,
+                        NULL);
 
   return cache;
 }
@@ -234,7 +236,8 @@ delete_queue_member (QueueMember *member, gpointer user_data)
   if (member)
     {
       g_clear_object (&member->texture);
-      g_free (member->key);
+      g_clear_pointer (&member->symbols, g_ptr_array_unref);
+      g_clear_pointer (&member->key, g_free);
       g_free (member);
     }
 }
@@ -259,7 +262,9 @@ shumate_memory_cache_clean (ShumateMemoryCache *memory_cache)
 
 
 gboolean
-shumate_memory_cache_try_fill_tile (ShumateMemoryCache *self, ShumateTile *tile, const char *source_id)
+shumate_memory_cache_try_fill_tile (ShumateMemoryCache *self,
+                                    ShumateTile        *tile,
+                                    const char         *source_id)
 {
   ShumateMemoryCache *memory_cache = (ShumateMemoryCache *) self;
   ShumateMemoryCachePrivate *priv = shumate_memory_cache_get_instance_private (memory_cache);
@@ -280,17 +285,17 @@ shumate_memory_cache_try_fill_tile (ShumateMemoryCache *self, ShumateTile *tile,
 
   move_queue_member_to_head (priv->queue, link);
 
-  if (!member->texture)
-    return FALSE;
-
   shumate_tile_set_texture (tile, member->texture);
+  shumate_tile_set_symbols (tile, member->symbols);
   shumate_tile_set_fade_in (tile, FALSE);
   shumate_tile_set_state (tile, SHUMATE_STATE_DONE);
   return TRUE;
 }
 
 void
-shumate_memory_cache_store_texture (ShumateMemoryCache *self, ShumateTile *tile, GdkTexture *texture, const 
char *source_id)
+shumate_memory_cache_store_tile (ShumateMemoryCache *self,
+                                 ShumateTile        *tile,
+                                 const char         *source_id)
 {
   ShumateMemoryCachePrivate *priv = shumate_memory_cache_get_instance_private (self);
   GList *link;
@@ -309,6 +314,8 @@ shumate_memory_cache_store_texture (ShumateMemoryCache *self, ShumateTile *tile,
   else
     {
       QueueMember *member;
+      GdkTexture *texture;
+      GPtrArray *symbols;
 
       if (priv->queue->length >= priv->size_limit)
         {
@@ -319,7 +326,10 @@ shumate_memory_cache_store_texture (ShumateMemoryCache *self, ShumateTile *tile,
 
       member = g_new0 (QueueMember, 1);
       member->key = key;
-      member->texture = g_object_ref (texture);
+      if ((texture = shumate_tile_get_texture (tile)))
+        member->texture = g_object_ref (texture);
+      if ((symbols = shumate_tile_get_symbols (tile)))
+        member->symbols = g_ptr_array_ref (symbols);
 
       g_queue_push_head (priv->queue, member);
       g_hash_table_insert (priv->hash_table, g_strdup (key), g_queue_peek_head_link (priv->queue));
diff --git a/shumate/shumate-memory-cache.h b/shumate/shumate-memory-cache.h
index 835115f..ff14da1 100644
--- a/shumate/shumate-memory-cache.h
+++ b/shumate/shumate-memory-cache.h
@@ -48,10 +48,9 @@ void shumate_memory_cache_clean (ShumateMemoryCache *memory_cache);
 gboolean shumate_memory_cache_try_fill_tile (ShumateMemoryCache *self,
                                              ShumateTile        *tile,
                                              const char         *source_id);
-void shumate_memory_cache_store_texture (ShumateMemoryCache *self,
-                                         ShumateTile        *tile,
-                                         GdkTexture         *texture,
-                                         const char         *source_id);
+void shumate_memory_cache_store_tile (ShumateMemoryCache *self,
+                                      ShumateTile        *tile,
+                                      const char         *source_id);
 
 G_END_DECLS
 
diff --git a/shumate/shumate-tile-private.h b/shumate/shumate-tile-private.h
new file mode 100644
index 0000000..9253ca4
--- /dev/null
+++ b/shumate/shumate-tile-private.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2021 James Westman <james jwestman net>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "shumate-tile.h"
+
+void shumate_tile_set_symbols (ShumateTile *self,
+                               GPtrArray   *symbols);
+
+GPtrArray *shumate_tile_get_symbols (ShumateTile *self);
diff --git a/shumate/shumate-tile.c b/shumate/shumate-tile.c
index 4c2a60b..4f8f7e4 100644
--- a/shumate/shumate-tile.c
+++ b/shumate/shumate-tile.c
@@ -46,6 +46,7 @@ typedef struct
   gboolean fade_in;
 
   GdkTexture *texture;
+  GPtrArray *symbols;
 } ShumateTilePrivate;
 
 G_DEFINE_TYPE_WITH_PRIVATE (ShumateTile, shumate_tile, GTK_TYPE_WIDGET);
@@ -204,6 +205,7 @@ shumate_tile_dispose (GObject *object)
   ShumateTilePrivate *priv = shumate_tile_get_instance_private (self);
 
   g_clear_object (&priv->texture);
+  g_clear_pointer (&priv->symbols, g_ptr_array_unref);
 
   G_OBJECT_CLASS (shumate_tile_parent_class)->dispose (object);
 }
@@ -662,3 +664,27 @@ shumate_tile_set_texture (ShumateTile *self,
       gtk_widget_queue_draw (GTK_WIDGET (self));
     }
 }
+
+
+void
+shumate_tile_set_symbols (ShumateTile *self, GPtrArray *symbols)
+{
+  ShumateTilePrivate *priv = shumate_tile_get_instance_private (self);
+
+  g_return_if_fail (SHUMATE_IS_TILE (self));
+
+  g_clear_pointer (&priv->symbols, g_ptr_array_unref);
+  if (symbols != NULL)
+    priv->symbols = g_ptr_array_ref (symbols);
+}
+
+
+GPtrArray *
+shumate_tile_get_symbols (ShumateTile *self)
+{
+  ShumateTilePrivate *priv = shumate_tile_get_instance_private (self);
+
+  g_return_val_if_fail (SHUMATE_IS_TILE (self), NULL);
+
+  return priv->symbols;
+}
diff --git a/shumate/shumate-vector-renderer.c b/shumate/shumate-vector-renderer.c
index 1b9046a..357ee44 100644
--- a/shumate/shumate-vector-renderer.c
+++ b/shumate/shumate-vector-renderer.c
@@ -17,6 +17,7 @@
 
 #include "shumate-vector-renderer.h"
 #include "shumate-tile-downloader.h"
+#include "shumate-tile-private.h"
 
 /**
  * ShumateVectorRenderer:
@@ -517,20 +518,28 @@ on_data_source_done (GObject *object, GAsyncResult *res, gpointer user_data)
     }
 }
 
-static GdkTexture *
-render (ShumateVectorRenderer *self, int texture_size, GBytes *tile_data, double zoom_level)
+static void
+render (ShumateVectorRenderer *self,
+        ShumateTile           *tile,
+        GBytes                *tile_data,
+        double                 zoom_level)
 {
 #ifdef SHUMATE_HAS_VECTOR_RENDERER
   ShumateVectorRenderScope scope;
-  GdkTexture *texture;
+  g_autoptr(GdkTexture) texture = NULL;
   cairo_surface_t *surface;
   gconstpointer data;
   gsize len;
+  g_autoptr(GPtrArray) symbols = g_ptr_array_new ();
+  int texture_size;
 
-  g_return_val_if_fail (SHUMATE_IS_VECTOR_RENDERER (self), NULL);
+  g_assert (SHUMATE_IS_VECTOR_RENDERER (self));
+  g_assert (SHUMATE_IS_TILE (tile));
 
+  texture_size = shumate_tile_get_size (tile);
   scope.target_size = texture_size;
   scope.zoom_level = zoom_level;
+  scope.symbols = symbols;
 
   surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, texture_size, texture_size);
   scope.cr = cairo_create (surface);
@@ -543,14 +552,14 @@ render (ShumateVectorRenderer *self, int texture_size, GBytes *tile_data, double
       shumate_vector_layer_render ((ShumateVectorLayer *)self->layers->pdata[i], &scope);
 
   texture = texture_new_for_surface (surface);
+  shumate_tile_set_texture (tile, texture);
+  shumate_tile_set_symbols (tile, symbols);
 
   cairo_destroy (scope.cr);
   cairo_surface_destroy (surface);
   vector_tile__tile__free_unpacked (scope.tile, NULL);
-
-  return texture;
 #else
-  g_return_val_if_reached (NULL);
+  g_return_if_reached ();
 #endif
 }
 
@@ -575,10 +584,7 @@ on_data_source_received_data (ShumateVectorRenderer *self,
       if (shumate_tile_get_x (tile) == x
           && shumate_tile_get_y (tile) == y
           && shumate_tile_get_zoom_level (tile) == zoom_level)
-        {
-          g_autoptr(GdkTexture) texture = render (self, shumate_tile_get_size (tile), bytes, zoom_level);
-          shumate_tile_set_texture (tile, texture);
-        }
+          render (self, tile, bytes, zoom_level);
     }
 }
 
diff --git a/shumate/vector/shumate-vector-expression.c b/shumate/vector/shumate-vector-expression.c
index b31ac54..5f6fe13 100644
--- a/shumate/vector/shumate-vector-expression.c
+++ b/shumate/vector/shumate-vector-expression.c
@@ -90,8 +90,12 @@ shumate_vector_expression_eval (ShumateVectorExpression  *self,
                                 ShumateVectorRenderScope *scope,
                                 ShumateVectorValue       *out)
 {
-  g_return_val_if_fail (SHUMATE_IS_VECTOR_EXPRESSION (self), FALSE);
-  return SHUMATE_VECTOR_EXPRESSION_GET_CLASS (self)->eval (self, scope, out);
+  g_return_val_if_fail (self == NULL || SHUMATE_IS_VECTOR_EXPRESSION (self), FALSE);
+
+  if (self == NULL)
+    return FALSE;
+  else
+    return SHUMATE_VECTOR_EXPRESSION_GET_CLASS (self)->eval (self, scope, out);
 }
 
 
diff --git a/shumate/vector/shumate-vector-layer.c b/shumate/vector/shumate-vector-layer.c
index f665e79..bd653dc 100644
--- a/shumate/vector/shumate-vector-layer.c
+++ b/shumate/vector/shumate-vector-layer.c
@@ -21,6 +21,7 @@
 #include "shumate-vector-fill-layer-private.h"
 #include "shumate-vector-layer-private.h"
 #include "shumate-vector-line-layer-private.h"
+#include "shumate-vector-symbol-layer-private.h"
 
 typedef struct
 {
@@ -56,6 +57,8 @@ shumate_vector_layer_create_from_json (JsonObject *object, GError **error)
     layer = shumate_vector_fill_layer_create_from_json (object, error);
   else if (g_strcmp0 (type, "line") == 0)
     layer = shumate_vector_line_layer_create_from_json (object, error);
+  else if (g_strcmp0 (type, "symbol") == 0)
+    layer = shumate_vector_symbol_layer_create_from_json (object, error);
   else
     {
       g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Unsupported layer type \"%s\"", type);
diff --git a/shumate/vector/shumate-vector-render-scope-private.h 
b/shumate/vector/shumate-vector-render-scope-private.h
index 52242f5..ac728a0 100644
--- a/shumate/vector/shumate-vector-render-scope-private.h
+++ b/shumate/vector/shumate-vector-render-scope-private.h
@@ -28,6 +28,8 @@ typedef struct {
   double scale;
   double zoom_level;
 
+  GPtrArray *symbols;
+
   VectorTile__Tile *tile;
   VectorTile__Tile__Layer *layer;
   VectorTile__Tile__Feature *feature;
@@ -36,4 +38,11 @@ typedef struct {
 
 gboolean shumate_vector_render_scope_find_layer (ShumateVectorRenderScope *self, const char *layer_name);
 void shumate_vector_render_scope_exec_geometry (ShumateVectorRenderScope *self);
+void shumate_vector_render_scope_get_geometry_center (ShumateVectorRenderScope *self, double *x, double *y);
+void shumate_vector_render_scope_get_bounds (ShumateVectorRenderScope *self,
+                                             double                   *min_x,
+                                             double                   *min_y,
+                                             double                   *max_x,
+                                             double                   *max_y);
+
 void shumate_vector_render_scope_get_variable (ShumateVectorRenderScope *self, const char *variable, 
ShumateVectorValue *value);
diff --git a/shumate/vector/shumate-vector-render-scope.c b/shumate/vector/shumate-vector-render-scope.c
index e544b4e..14e7644 100644
--- a/shumate/vector/shumate-vector-render-scope.c
+++ b/shumate/vector/shumate-vector-render-scope.c
@@ -87,6 +87,70 @@ shumate_vector_render_scope_exec_geometry (ShumateVectorRenderScope *self)
 }
 
 
+void
+shumate_vector_render_scope_get_bounds (ShumateVectorRenderScope *self,
+                                        double                   *min_x,
+                                        double                   *min_y,
+                                        double                   *max_x,
+                                        double                   *max_y)
+{
+  double x = 0, y = 0;
+
+  *min_x = self->layer->extent;
+  *min_y = self->layer->extent;
+  *max_x = 0;
+  *max_y = 0;
+
+  g_return_if_fail (self->feature != NULL);
+
+  for (int i = 0; i < self->feature->n_geometry; i ++)
+    {
+      int cmd = self->feature->geometry[i];
+
+      /* See https://github.com/mapbox/vector-tile-spec/tree/master/2.1#43-geometry-encoding */
+      int op = cmd & 0x7;
+      int repeat = cmd >> 3;
+
+      for (int j = 0; j < repeat; j ++)
+        {
+          switch (op) {
+          case 1:
+            g_return_if_fail (i + 2 < self->feature->n_geometry);
+            x += zigzag (self->feature->geometry[++i]);
+            y += zigzag (self->feature->geometry[++i]);
+            break;
+          case 2:
+            g_return_if_fail (i + 2 < self->feature->n_geometry);
+            x += zigzag (self->feature->geometry[++i]);
+            y += zigzag (self->feature->geometry[++i]);
+            break;
+          case 7:
+            break;
+          default:
+            g_assert_not_reached ();
+          }
+
+          *min_x = MIN (*min_x, x);
+          *min_y = MIN (*min_y, y);
+          *max_x = MAX (*max_x, x);
+          *max_y = MAX (*max_y, y);
+        }
+    }
+}
+
+
+void
+shumate_vector_render_scope_get_geometry_center (ShumateVectorRenderScope *self,
+                                                 double                   *x,
+                                                 double                   *y)
+{
+  double min_x, min_y, max_x, max_y;
+  shumate_vector_render_scope_get_bounds (self, &min_x, &min_y, &max_x, &max_y);
+  *x = (min_x + max_x) / 2.0 / self->layer->extent;
+  *y = (min_y + max_y) / 2.0 / self->layer->extent;
+}
+
+
 void
 shumate_vector_render_scope_get_variable (ShumateVectorRenderScope *self, const char *variable, 
ShumateVectorValue *value)
 {
diff --git a/shumate/vector/shumate-vector-symbol-container-private.h 
b/shumate/vector/shumate-vector-symbol-container-private.h
new file mode 100644
index 0000000..33f752e
--- /dev/null
+++ b/shumate/vector/shumate-vector-symbol-container-private.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 James Westman <james jwestman net>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "shumate-map-source.h"
+#include "shumate-layer.h"
+
+G_BEGIN_DECLS
+
+
+#define SHUMATE_TYPE_VECTOR_SYMBOL_CONTAINER (shumate_vector_symbol_container_get_type())
+G_DECLARE_FINAL_TYPE (ShumateVectorSymbolContainer, shumate_vector_symbol_container, SHUMATE, 
VECTOR_SYMBOL_CONTAINER, ShumateLayer)
+
+ShumateVectorSymbolContainer *shumate_vector_symbol_container_new (ShumateMapSource *map_source,
+                                                                   ShumateViewport  *viewport);
+
+void shumate_vector_symbol_container_add_symbols (ShumateVectorSymbolContainer *self,
+                                                  GPtrArray                    *symbol_infos,
+                                                  int                           tile_x,
+                                                  int                           tile_y,
+                                                  int                           zoom);
+
+void shumate_vector_symbol_container_remove_symbols (ShumateVectorSymbolContainer *self,
+                                                     int                           tile_x,
+                                                     int                           tile_y,
+                                                     int                           zoom);
+
+G_END_DECLS
diff --git a/shumate/vector/shumate-vector-symbol-container.c 
b/shumate/vector/shumate-vector-symbol-container.c
new file mode 100644
index 0000000..2616c97
--- /dev/null
+++ b/shumate/vector/shumate-vector-symbol-container.c
@@ -0,0 +1,318 @@
+/*
+ * Copyright (C) 2021 James Westman <james jwestman net>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "shumate-vector-symbol-container-private.h"
+#include "shumate-vector-symbol-private.h"
+
+
+struct _ShumateVectorSymbolContainer
+{
+  ShumateLayer parent_instance;
+
+  ShumateMapSource *map_source;
+
+  GList *children;
+};
+
+G_DEFINE_TYPE (ShumateVectorSymbolContainer, shumate_vector_symbol_container, SHUMATE_TYPE_LAYER)
+
+enum {
+  PROP_0,
+  PROP_MAP_SOURCE,
+  N_PROPS,
+};
+
+static GParamSpec *obj_properties[N_PROPS] = { NULL, };
+
+
+typedef struct {
+  // does not need to be freed because it's owned by the widget
+  ShumateVectorSymbol *symbol;
+
+  // These are coordinates [0, 1) within the tile
+  float x;
+  float y;
+
+  // We assume these don't change so we don't have to measure them again
+  // every time. We can do this because children are all created internally
+  int width;
+  int height;
+
+  int tile_x;
+  int tile_y;
+  int zoom;
+} ChildInfo;
+
+
+ShumateVectorSymbolContainer *
+shumate_vector_symbol_container_new (ShumateMapSource *map_source,
+                                     ShumateViewport  *viewport)
+{
+  return g_object_new (SHUMATE_TYPE_VECTOR_SYMBOL_CONTAINER,
+                       "map-source", map_source,
+                       "viewport", viewport,
+                       NULL);
+}
+
+
+static void
+on_viewport_changed (ShumateVectorSymbolContainer  *self,
+                     G_GNUC_UNUSED GParamSpec      *pspec,
+                     G_GNUC_UNUSED ShumateViewport *view)
+{
+  gtk_widget_queue_allocate (GTK_WIDGET (self));
+}
+
+
+static void
+shumate_vector_symbol_container_constructed (GObject *object)
+{
+  ShumateVectorSymbolContainer *self = (ShumateVectorSymbolContainer *)object;
+  ShumateViewport *viewport;
+
+  G_OBJECT_CLASS (shumate_vector_symbol_container_parent_class)->constructed (object);
+
+  viewport = shumate_layer_get_viewport (SHUMATE_LAYER (self));
+
+  g_signal_connect_swapped (viewport, "notify::longitude", G_CALLBACK (on_viewport_changed), self);
+  g_signal_connect_swapped (viewport, "notify::latitude", G_CALLBACK (on_viewport_changed), self);
+  g_signal_connect_swapped (viewport, "notify::zoom-level", G_CALLBACK (on_viewport_changed), self);
+  g_signal_connect_swapped (viewport, "notify::rotation", G_CALLBACK (on_viewport_changed), self);
+}
+
+
+static void
+shumate_vector_symbol_container_finalize (GObject *object)
+{
+  ShumateVectorSymbolContainer *self = (ShumateVectorSymbolContainer *)object;
+
+  g_list_free_full (self->children, (GDestroyNotify) g_free);
+
+  G_OBJECT_CLASS (shumate_vector_symbol_container_parent_class)->finalize (object);
+}
+
+
+static void
+shumate_vector_symbol_container_dispose (GObject *object)
+{
+  ShumateVectorSymbolContainer *self = (ShumateVectorSymbolContainer *)object;
+  ShumateViewport *viewport = shumate_layer_get_viewport (SHUMATE_LAYER (self));
+  GtkWidget *child;
+
+  g_signal_handlers_disconnect_by_data (viewport, self);
+
+  while ((child = gtk_widget_get_first_child (GTK_WIDGET (object))))
+    gtk_widget_unparent (child);
+
+  G_OBJECT_CLASS (shumate_vector_symbol_container_parent_class)->dispose (object);
+}
+
+
+static void
+shumate_vector_symbol_container_get_property (GObject    *object,
+                                              guint       property_id,
+                                              GValue     *value,
+                                              GParamSpec *pspec)
+{
+  ShumateVectorSymbolContainer *self = (ShumateVectorSymbolContainer *)object;
+
+  switch (property_id)
+    {
+    case PROP_MAP_SOURCE:
+      g_value_set_object (value, self->map_source);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    }
+}
+
+
+static void
+shumate_vector_symbol_container_set_property (GObject      *object,
+                                              guint         property_id,
+                                              const GValue *value,
+                                              GParamSpec   *pspec)
+{
+  ShumateVectorSymbolContainer *self = SHUMATE_VECTOR_SYMBOL_CONTAINER (object);
+
+  switch (property_id)
+    {
+    case PROP_MAP_SOURCE:
+      g_set_object (&self->map_source, g_value_get_object (value));
+      gtk_widget_queue_allocate (GTK_WIDGET (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    }
+}
+
+
+static void
+rotate_around_center (float *x,
+                      float *y,
+                      float  width,
+                      float  height,
+                      float  angle)
+{
+  /* Rotate (x, y) around (width / 2, height / 2) */
+
+  float old_x = *x;
+  float old_y = *y;
+  float center_x = width / 2.0;
+  float center_y = height / 2.0;
+
+  *x = cosf (angle) * (old_x - center_x) - sinf (angle) * (old_y - center_y) + center_x;
+  *y = sinf (angle) * (old_x - center_x) + cosf (angle) * (old_y - center_y) + center_y;
+}
+
+
+static void
+shumate_vector_symbol_container_size_allocate (GtkWidget *widget,
+                                               int        width,
+                                               int        height,
+                                               int        baseline)
+{
+  ShumateVectorSymbolContainer *self = SHUMATE_VECTOR_SYMBOL_CONTAINER (widget);
+  GtkAllocation alloc;
+  float tile_size = shumate_map_source_get_tile_size (self->map_source);
+  ShumateViewport *viewport = shumate_layer_get_viewport (SHUMATE_LAYER (self));
+  float zoom_level = shumate_viewport_get_zoom_level (viewport);
+  float rotation = shumate_viewport_get_rotation (viewport);
+  float center_x = shumate_map_source_get_x (self->map_source, zoom_level, shumate_location_get_longitude 
(SHUMATE_LOCATION (viewport)));
+  float center_y = shumate_map_source_get_y (self->map_source, zoom_level, shumate_location_get_latitude 
(SHUMATE_LOCATION (viewport)));
+
+  for (GList *l = self->children; l != NULL; l = l->next)
+    {
+      ChildInfo *child = l->data;
+      float tile_size_at_zoom = tile_size * powf (2, zoom_level - child->zoom);
+      float x = (child->tile_x + child->x) * tile_size_at_zoom - center_x + width/2.0;
+      float y = (child->tile_y + child->y) * tile_size_at_zoom - center_y + height/2.0;
+
+      rotate_around_center (&x, &y, width, height, rotation);
+      alloc.x = x - child->width/2.0;
+      alloc.y = y - child->height/2.0;
+
+      alloc.width = child->width;
+      alloc.height = child->height;
+
+      gtk_widget_size_allocate (GTK_WIDGET (child->symbol), &alloc, -1);
+    }
+}
+
+
+static void
+shumate_vector_symbol_container_snapshot (GtkWidget   *widget,
+                                          GtkSnapshot *snapshot)
+{
+  ShumateVectorSymbolContainer *self = SHUMATE_VECTOR_SYMBOL_CONTAINER (widget);
+
+  for (GList *l = self->children; l != NULL; l = l->next)
+    {
+      ChildInfo *child = (ChildInfo *)l->data;
+
+      gtk_snapshot_save (snapshot);
+
+      gtk_widget_snapshot_child (widget, GTK_WIDGET (child->symbol), snapshot);
+      gtk_snapshot_restore (snapshot);
+    }
+
+}
+
+
+static void
+shumate_vector_symbol_container_class_init (ShumateVectorSymbolContainerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->constructed = shumate_vector_symbol_container_constructed;
+  object_class->dispose = shumate_vector_symbol_container_dispose;
+  object_class->finalize = shumate_vector_symbol_container_finalize;
+  object_class->get_property = shumate_vector_symbol_container_get_property;
+  object_class->set_property = shumate_vector_symbol_container_set_property;
+  widget_class->size_allocate = shumate_vector_symbol_container_size_allocate;
+  widget_class->snapshot = shumate_vector_symbol_container_snapshot;
+
+  obj_properties[PROP_MAP_SOURCE] =
+    g_param_spec_object ("map-source",
+                         "Map source",
+                         "Map source",
+                         SHUMATE_TYPE_MAP_SOURCE,
+                         G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+  g_object_class_install_properties (object_class, N_PROPS, obj_properties);
+}
+
+static void
+shumate_vector_symbol_container_init (ShumateVectorSymbolContainer *self)
+{
+}
+
+
+void
+shumate_vector_symbol_container_add_symbols (ShumateVectorSymbolContainer *self,
+                                             GPtrArray                    *symbol_infos,
+                                             int                           tile_x,
+                                             int                           tile_y,
+                                             int                           zoom)
+{
+  g_return_if_fail (SHUMATE_IS_VECTOR_SYMBOL_CONTAINER (self));
+
+  for (int i = 0; i < symbol_infos->len; i ++)
+    {
+      ChildInfo *info = g_new0 (ChildInfo, 1);
+      ShumateVectorSymbolInfo *symbol_info = symbol_infos->pdata[i];
+      ShumateVectorSymbol *symbol = shumate_vector_symbol_new (symbol_info);
+
+      info->symbol = symbol;
+      gtk_widget_measure (GTK_WIDGET (symbol), GTK_ORIENTATION_HORIZONTAL, -1, NULL, &info->width, NULL, 
NULL);
+      gtk_widget_measure (GTK_WIDGET (symbol), GTK_ORIENTATION_VERTICAL, -1, NULL, &info->height, NULL, 
NULL);
+      info->x = symbol_info->x;
+      info->y = symbol_info->y;
+      info->tile_x = tile_x;
+      info->tile_y = tile_y;
+      info->zoom = zoom;
+
+      self->children = g_list_prepend (self->children, info);
+      gtk_widget_set_parent (GTK_WIDGET (info->symbol), GTK_WIDGET (self));
+    }
+}
+
+
+void
+shumate_vector_symbol_container_remove_symbols (ShumateVectorSymbolContainer *self,
+                                                int                           tile_x,
+                                                int                           tile_y,
+                                                int                           zoom)
+{
+  g_return_if_fail (SHUMATE_IS_VECTOR_SYMBOL_CONTAINER (self));
+
+  for (GList *l = self->children; l != NULL; l = l->next)
+    {
+      ChildInfo *info = l->data;
+
+      if (info->tile_x != tile_x || info->tile_y != tile_y || info->zoom != zoom)
+        continue;
+
+      gtk_widget_unparent (GTK_WIDGET (info->symbol));
+      g_clear_pointer (&l->data, g_free);
+    }
+
+  self->children = g_list_remove_all (self->children, NULL);
+}
diff --git a/shumate/vector/shumate-vector-symbol-info-private.h 
b/shumate/vector/shumate-vector-symbol-info-private.h
new file mode 100644
index 0000000..3552f8f
--- /dev/null
+++ b/shumate/vector/shumate-vector-symbol-info-private.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2021 James Westman <james jwestman net>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+
+#pragma once
+
+#include <glib-object.h>
+#include "shumate-vector-render-scope-private.h"
+
+
+G_BEGIN_DECLS
+
+#define SHUMATE_TYPE_VECTOR_SYMBOL_INFO (shumate_vector_symbol_info_get_type ())
+
+typedef struct _ShumateVectorSymbolInfo ShumateVectorSymbolInfo;
+
+struct _ShumateVectorSymbolInfo
+{
+  char *text;
+  GdkRGBA text_color;
+  double text_size;
+  char *text_font;
+  gboolean line_placement;
+  double x;
+  double y;
+
+  /*< private >*/
+  guint ref_count;
+};
+
+ShumateVectorSymbolInfo *shumate_vector_symbol_info_new (const char               *text,
+                                                         const GdkRGBA            *text_color,
+                                                         double                    text_size,
+                                                         const char               *text_font,
+                                                         gboolean                  line_placement,
+                                                         double                    x,
+                                                         double                    y);
+
+GType                        shumate_vector_symbol_info_get_type (void) G_GNUC_CONST;
+ShumateVectorSymbolInfo     *shumate_vector_symbol_info_ref      (ShumateVectorSymbolInfo *self);
+void                         shumate_vector_symbol_info_unref    (ShumateVectorSymbolInfo *self);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (ShumateVectorSymbolInfo, shumate_vector_symbol_info_unref)
+
+G_END_DECLS
+
diff --git a/shumate/vector/shumate-vector-symbol-info.c b/shumate/vector/shumate-vector-symbol-info.c
new file mode 100644
index 0000000..de8a605
--- /dev/null
+++ b/shumate/vector/shumate-vector-symbol-info.c
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2021 James Westman <james jwestman net>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+
+#include "shumate-vector-symbol-info-private.h"
+
+
+G_DEFINE_BOXED_TYPE (ShumateVectorSymbolInfo, shumate_vector_symbol_info, shumate_vector_symbol_info_ref, 
shumate_vector_symbol_info_unref)
+
+
+static void
+shumate_vector_symbol_info_free (ShumateVectorSymbolInfo *self)
+{
+  g_assert (self);
+  g_assert_cmpint (self->ref_count, ==, 0);
+
+  g_clear_pointer (&self->text, g_free);
+  g_clear_pointer (&self->text_font, g_free);
+
+  g_free (self);
+}
+
+ShumateVectorSymbolInfo *
+shumate_vector_symbol_info_ref (ShumateVectorSymbolInfo *self)
+{
+  g_return_val_if_fail (self, NULL);
+  g_return_val_if_fail (self->ref_count, NULL);
+
+  g_atomic_int_inc (&self->ref_count);
+
+  return self;
+}
+
+void
+shumate_vector_symbol_info_unref (ShumateVectorSymbolInfo *self)
+{
+  g_return_if_fail (self);
+  g_return_if_fail (self->ref_count);
+
+  if (g_atomic_int_dec_and_test (&self->ref_count))
+    shumate_vector_symbol_info_free (self);
+}
+
+
+ShumateVectorSymbolInfo *
+shumate_vector_symbol_info_new (const char    *text,
+                                const GdkRGBA *text_color,
+                                double         text_size,
+                                const char    *text_font,
+                                gboolean       line_placement,
+                                double         x,
+                                double         y)
+{
+  ShumateVectorSymbolInfo *self;
+
+  self = g_new0 (ShumateVectorSymbolInfo, 1);
+
+  *self = (ShumateVectorSymbolInfo) {
+    .ref_count = 1,
+    .text = g_strdup (text),
+    .text_color = *text_color,
+    .text_size = text_size,
+    .text_font = g_strdup (text_font),
+    .line_placement = line_placement,
+    .x = x,
+    .y = y,
+  };
+
+  return self;
+}
+
diff --git a/shumate/vector/shumate-vector-symbol-layer-private.h 
b/shumate/vector/shumate-vector-symbol-layer-private.h
new file mode 100644
index 0000000..34e1953
--- /dev/null
+++ b/shumate/vector/shumate-vector-symbol-layer-private.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2021 James Westman <james jwestman net>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <json-glib/json-glib.h>
+
+#include "shumate-vector-layer-private.h"
+
+G_BEGIN_DECLS
+
+#define SHUMATE_TYPE_VECTOR_SYMBOL_LAYER (shumate_vector_symbol_layer_get_type())
+G_DECLARE_FINAL_TYPE (ShumateVectorSymbolLayer, shumate_vector_symbol_layer, SHUMATE, VECTOR_SYMBOL_LAYER, 
ShumateVectorLayer)
+
+ShumateVectorLayer *shumate_vector_symbol_layer_create_from_json (JsonObject *object, GError **error);
+
+G_END_DECLS
diff --git a/shumate/vector/shumate-vector-symbol-layer.c b/shumate/vector/shumate-vector-symbol-layer.c
new file mode 100644
index 0000000..d57589d
--- /dev/null
+++ b/shumate/vector/shumate-vector-symbol-layer.c
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2021 James Westman <james jwestman net>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <gtk/gtk.h>
+#include "shumate-vector-symbol-info-private.h"
+#include "shumate-vector-symbol-layer-private.h"
+#include "shumate-vector-expression-private.h"
+#include "shumate-vector-utils-private.h"
+
+struct _ShumateVectorSymbolLayer
+{
+  ShumateVectorLayer parent_instance;
+
+  ShumateVectorExpression *text_field;
+  ShumateVectorExpression *text_color;
+  ShumateVectorExpression *text_size;
+  gboolean line_placement;
+  char *text_fonts;
+};
+
+G_DEFINE_TYPE (ShumateVectorSymbolLayer, shumate_vector_symbol_layer, SHUMATE_TYPE_VECTOR_LAYER)
+
+
+ShumateVectorLayer *
+shumate_vector_symbol_layer_create_from_json (JsonObject *object, GError **error)
+{
+  ShumateVectorSymbolLayer *layer = g_object_new (SHUMATE_TYPE_VECTOR_SYMBOL_LAYER, NULL);
+
+  if (json_object_has_member (object, "paint"))
+    {
+      JsonObject *paint = json_object_get_object_member (object, "paint");
+
+      layer->text_color = shumate_vector_expression_from_json (json_object_get_member (paint, "text-color"), 
error);
+      if (layer->text_color == NULL)
+        return NULL;
+    }
+
+  if (json_object_has_member (object, "layout"))
+    {
+      JsonObject *layout = json_object_get_object_member (object, "layout");
+      JsonNode *text_font_node;
+      JsonArray *text_font;
+
+      layer->text_field = shumate_vector_expression_from_json (json_object_get_member (layout, 
"text-field"), error);
+      if (layer->text_field == NULL)
+        return NULL;
+
+      text_font_node = json_object_get_member (layout, "text-font");
+      if (text_font_node != NULL)
+        {
+          g_autoptr(GStrvBuilder) builder = g_strv_builder_new ();
+          g_auto(GStrv) fonts = NULL;
+
+          if (!shumate_vector_json_get_array (text_font_node, &text_font, error))
+            return NULL;
+
+          for (int i = 0, n = json_array_get_length (text_font); i < n; i ++)
+            g_strv_builder_add (builder, json_array_get_string_element (text_font, i));
+
+          fonts = g_strv_builder_end (builder);
+          layer->text_fonts = g_strjoinv (", ", fonts);
+        }
+
+      layer->line_placement = g_strcmp0 (json_object_get_string_member_with_default (layout, 
"symbol-placement", NULL), "line") == 0;
+
+      layer->text_size = shumate_vector_expression_from_json (json_object_get_member (layout, "text-size"), 
error);
+      if (layer->text_size == NULL)
+        return NULL;
+    }
+
+  return (ShumateVectorLayer *)layer;
+}
+
+
+static void
+shumate_vector_symbol_layer_finalize (GObject *object)
+{
+  ShumateVectorSymbolLayer *self = SHUMATE_VECTOR_SYMBOL_LAYER (object);
+
+  g_clear_object (&self->text_field);
+  g_clear_object (&self->text_color);
+  g_clear_object (&self->text_size);
+  g_clear_pointer (&self->text_fonts, g_free);
+
+  G_OBJECT_CLASS (shumate_vector_symbol_layer_parent_class)->finalize (object);
+}
+
+
+static void
+shumate_vector_symbol_layer_render (ShumateVectorLayer *layer, ShumateVectorRenderScope *scope)
+{
+  ShumateVectorSymbolLayer *self = SHUMATE_VECTOR_SYMBOL_LAYER (layer);
+  g_autofree char *text_field = NULL;
+  GdkRGBA text_color = SHUMATE_VECTOR_COLOR_BLACK;
+  double text_size;
+  ShumateVectorSymbolInfo *symbol_info;
+  double x, y;
+
+  shumate_vector_render_scope_get_geometry_center (scope, &x, &y);
+  if (x < 0 || x >= 1 || y < 0 || y >= 1)
+    /* Tiles usually include a bit of margin. Don't include symbols that are
+     * covered by a different tile. */
+    return;
+
+  shumate_vector_expression_eval_color (self->text_color, scope, &text_color);
+  text_size = shumate_vector_expression_eval_number (self->text_size, scope, 16.0);
+  text_field = shumate_vector_expression_eval_string (self->text_field, scope, "");
+
+  if (strlen (text_field) == 0)
+    return;
+
+  symbol_info = shumate_vector_symbol_info_new (text_field,
+                                                &text_color,
+                                                text_size,
+                                                self->text_fonts,
+                                                self->line_placement,
+                                                x,
+                                                y);
+  g_ptr_array_add (scope->symbols, symbol_info);
+}
+
+
+static void
+shumate_vector_symbol_layer_class_init (ShumateVectorSymbolLayerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  ShumateVectorLayerClass *layer_class = SHUMATE_VECTOR_LAYER_CLASS (klass);
+
+  object_class->finalize = shumate_vector_symbol_layer_finalize;
+  layer_class->render = shumate_vector_symbol_layer_render;
+}
+
+
+static void
+shumate_vector_symbol_layer_init (ShumateVectorSymbolLayer *self)
+{
+}
diff --git a/shumate/vector/shumate-vector-symbol-private.h b/shumate/vector/shumate-vector-symbol-private.h
new file mode 100644
index 0000000..3494595
--- /dev/null
+++ b/shumate/vector/shumate-vector-symbol-private.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2021 James Westman <james jwestman net>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include "shumate-vector-symbol-info-private.h"
+
+G_BEGIN_DECLS
+
+#define SHUMATE_TYPE_VECTOR_SYMBOL (shumate_vector_symbol_get_type())
+G_DECLARE_FINAL_TYPE (ShumateVectorSymbol, shumate_vector_symbol, SHUMATE, VECTOR_SYMBOL, GtkWidget)
+
+ShumateVectorSymbol *shumate_vector_symbol_new (ShumateVectorSymbolInfo *symbol_info);
+
+ShumateVectorSymbolInfo *shumate_vector_symbol_get_symbol_info (ShumateVectorSymbol *self);
+
+G_END_DECLS
diff --git a/shumate/vector/shumate-vector-symbol.c b/shumate/vector/shumate-vector-symbol.c
new file mode 100644
index 0000000..ed65242
--- /dev/null
+++ b/shumate/vector/shumate-vector-symbol.c
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2021 James Westman <james jwestman net>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+
+#include "shumate-vector-symbol-private.h"
+#include "shumate-vector-utils-private.h"
+#include "shumate-vector-symbol-info-private.h"
+
+
+struct _ShumateVectorSymbol
+{
+  GtkWidget parent_instance;
+
+  ShumateVectorSymbolInfo *symbol_info;
+};
+
+G_DEFINE_TYPE (ShumateVectorSymbol, shumate_vector_symbol, GTK_TYPE_WIDGET)
+
+
+enum {
+  PROP_0,
+  PROP_SYMBOL_INFO,
+  N_PROPS,
+};
+
+static GParamSpec *obj_properties[N_PROPS] = { NULL, };
+
+
+ShumateVectorSymbol *
+shumate_vector_symbol_new (ShumateVectorSymbolInfo *symbol_info)
+{
+  return g_object_new (SHUMATE_TYPE_VECTOR_SYMBOL,
+                       "symbol-info", symbol_info,
+                       NULL);
+}
+
+
+static void
+shumate_vector_symbol_constructed (GObject *object)
+{
+  ShumateVectorSymbol *self = (ShumateVectorSymbol *)object;
+  g_autoptr(PangoAttrList) attrs = pango_attr_list_new ();
+  PangoAttribute *attr;
+
+  if (self->symbol_info->text_font != NULL)
+    {
+      g_autoptr(PangoFontDescription) desc = pango_font_description_from_string 
(self->symbol_info->text_font);
+      attr = pango_attr_font_desc_new (desc);
+      pango_attr_list_insert (attrs, attr);
+    }
+
+  attr = pango_attr_foreground_new (self->symbol_info->text_color.red * 65535,
+                                    self->symbol_info->text_color.green * 65535,
+                                    self->symbol_info->text_color.blue * 65535);
+  pango_attr_list_insert (attrs, attr);
+
+  attr = pango_attr_foreground_alpha_new (self->symbol_info->text_color.alpha * 65535);
+  pango_attr_list_insert (attrs, attr);
+
+  attr = pango_attr_size_new_absolute (self->symbol_info->text_size * PANGO_SCALE);
+  pango_attr_list_insert (attrs, attr);
+
+  GtkWidget *label = gtk_label_new (self->symbol_info->text);
+  gtk_label_set_attributes (GTK_LABEL (label), attrs);
+  gtk_widget_set_parent (label, GTK_WIDGET (self));
+
+  G_OBJECT_CLASS (shumate_vector_symbol_parent_class)->constructed (object);
+}
+
+
+static void
+shumate_vector_symbol_dispose (GObject *object)
+{
+  ShumateVectorSymbol *self = (ShumateVectorSymbol *)object;
+  GtkWidget *child;
+
+  while ((child = gtk_widget_get_first_child (GTK_WIDGET (object))))
+    gtk_widget_unparent (child);
+
+  g_clear_pointer (&self->symbol_info, shumate_vector_symbol_info_unref);
+
+  G_OBJECT_CLASS (shumate_vector_symbol_parent_class)->dispose (object);
+}
+
+
+static void
+shumate_vector_symbol_get_property (GObject    *object,
+                                    guint       property_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  ShumateVectorSymbol *self = (ShumateVectorSymbol *)object;
+
+  switch (property_id)
+    {
+    case PROP_SYMBOL_INFO:
+      g_value_set_boxed (value, self->symbol_info);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    }
+}
+
+
+static void
+shumate_vector_symbol_set_property (GObject      *object,
+                                    guint         property_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+  ShumateVectorSymbol *self = SHUMATE_VECTOR_SYMBOL (object);
+
+  switch (property_id)
+    {
+    case PROP_SYMBOL_INFO:
+      g_assert (self->symbol_info == NULL);
+      self->symbol_info = g_value_dup_boxed (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    }
+}
+
+
+static void
+shumate_vector_symbol_class_init (ShumateVectorSymbolClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->constructed = shumate_vector_symbol_constructed;
+  object_class->dispose = shumate_vector_symbol_dispose;
+  object_class->get_property = shumate_vector_symbol_get_property;
+  object_class->set_property = shumate_vector_symbol_set_property;
+
+  obj_properties[PROP_SYMBOL_INFO] =
+    g_param_spec_boxed ("symbol-info",
+                        "Symbol info",
+                        "Symbol info",
+                        SHUMATE_TYPE_VECTOR_SYMBOL_INFO,
+                        G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+  g_object_class_install_properties (object_class, N_PROPS, obj_properties);
+
+  gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
+}
+
+
+static void
+shumate_vector_symbol_init (ShumateVectorSymbol *self)
+{
+}
+
+
+ShumateVectorSymbolInfo *
+shumate_vector_symbol_get_symbol_info (ShumateVectorSymbol *self)
+{
+  g_return_val_if_fail (SHUMATE_IS_VECTOR_SYMBOL (self), NULL);
+
+  return self->symbol_info;
+}
diff --git a/tests/memory-cache.c b/tests/memory-cache.c
index 95670a7..bb885ca 100644
--- a/tests/memory-cache.c
+++ b/tests/memory-cache.c
@@ -20,7 +20,8 @@ test_memory_cache_store_retrieve ()
   g_object_ref_sink (tile);
 
   /* Store the tile */
-  shumate_memory_cache_store_texture (cache, tile, texture, "A");
+  shumate_tile_set_texture (tile, texture);
+  shumate_memory_cache_store_tile (cache, tile, "A");
 
   /* Now retrieve it */
   g_assert_true (shumate_memory_cache_try_fill_tile (cache, tile, "A"));
@@ -41,7 +42,8 @@ test_memory_cache_miss ()
   g_object_ref_sink (tile2);
 
   /* Store a tile */
-  shumate_memory_cache_store_texture (cache, tile1, texture, "A");
+  shumate_tile_set_texture (tile1, texture);
+  shumate_memory_cache_store_tile (cache, tile1, "A");
 
   /* Now retrieve a different one */
   g_assert_false (shumate_memory_cache_try_fill_tile (cache, tile2, "A"));
@@ -54,22 +56,26 @@ static void
 test_memory_cache_source_id ()
 {
   g_autoptr(ShumateMemoryCache) cache = shumate_memory_cache_new_full (100);
-  g_autoptr(ShumateTile) tile = shumate_tile_new_full (0, 0, 256, 0);
+  g_autoptr(ShumateTile) tile1 = shumate_tile_new_full (0, 0, 256, 0);
+  g_autoptr(ShumateTile) tile2 = shumate_tile_new_full (0, 0, 256, 0);
   g_autoptr(GdkTexture) texture1 = create_texture ();
   g_autoptr(GdkTexture) texture2 = create_texture ();
 
-  g_object_ref_sink (tile);
+  g_object_ref_sink (tile1);
+  g_object_ref_sink (tile2);
 
   /* Store the tiles */
-  shumate_memory_cache_store_texture (cache, tile, texture1, "A");
-  shumate_memory_cache_store_texture (cache, tile, texture2, "B");
+  shumate_tile_set_texture (tile1, texture1);
+  shumate_tile_set_texture (tile2, texture2);
+  shumate_memory_cache_store_tile (cache, tile1, "A");
+  shumate_memory_cache_store_tile (cache, tile2, "B");
 
   /* Now retrieve them */
-  g_assert_true (shumate_memory_cache_try_fill_tile (cache, tile, "A"));
-  g_assert_true (texture1 == shumate_tile_get_texture (tile));
+  g_assert_true (shumate_memory_cache_try_fill_tile (cache, tile1, "A"));
+  g_assert_true (texture1 == shumate_tile_get_texture (tile1));
 
-  g_assert_true (shumate_memory_cache_try_fill_tile (cache, tile, "B"));
-  g_assert_true (texture2 == shumate_tile_get_texture (tile));
+  g_assert_true (shumate_memory_cache_try_fill_tile (cache, tile2, "B"));
+  g_assert_true (texture2 == shumate_tile_get_texture (tile2));
 }
 
 
@@ -79,14 +85,13 @@ test_memory_cache_purge ()
 {
   g_autoptr(ShumateMemoryCache) cache = shumate_memory_cache_new_full (3);
   g_autoptr(ShumateTile) tile = shumate_tile_new_full (0, 0, 256, 0);
-  g_autoptr(GdkTexture) texture = create_texture ();
 
   g_object_ref_sink (tile);
 
   /* Store a few tiles */
-  shumate_memory_cache_store_texture (cache, tile, texture, "A");
-  shumate_memory_cache_store_texture (cache, tile, texture, "B");
-  shumate_memory_cache_store_texture (cache, tile, texture, "C");
+  shumate_memory_cache_store_tile (cache, tile, "A");
+  shumate_memory_cache_store_tile (cache, tile, "B");
+  shumate_memory_cache_store_tile (cache, tile, "C");
 
   /* Make sure they're all still cached */
   g_assert_true (shumate_memory_cache_try_fill_tile (cache, tile, "B"));
@@ -94,7 +99,7 @@ test_memory_cache_purge ()
   g_assert_true (shumate_memory_cache_try_fill_tile (cache, tile, "C"));
 
   /* Store another one */
-  shumate_memory_cache_store_texture (cache, tile, texture, "D");
+  shumate_memory_cache_store_tile (cache, tile, "D");
 
   /* Since B was the least recently accessed, it should be the one that was
    * dropped */
@@ -111,12 +116,11 @@ test_memory_cache_clean ()
 {
   g_autoptr(ShumateMemoryCache) cache = shumate_memory_cache_new_full (100);
   g_autoptr(ShumateTile) tile = shumate_tile_new_full (0, 0, 256, 0);
-  g_autoptr(GdkTexture) texture = create_texture ();
 
   g_object_ref_sink (tile);
 
   /* Store a tile */
-  shumate_memory_cache_store_texture (cache, tile, texture, "A");
+  shumate_memory_cache_store_tile (cache, tile, "A");
 
   /* Clean the cache */
   shumate_memory_cache_clean (cache);


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]