[libshumate] Split ShumateNetworkTileSource



commit 8c490eb2f27c9348ea9b7e38752537eaefcb7f17
Author: James Westman <james jwestman net>
Date:   Wed Nov 17 00:18:51 2021 -0600

    Split ShumateNetworkTileSource
    
    Split the network source into separate classes for fetching data,
    rendering raster tiles, and rendering vector tiles. This makes the code
    much more modular and organized, especially since vector tiles are about
    to get even more complicated.
    
    ShumateDataSource is a new abstract class that retrieves tile data. It
    has a built-in implementation, ShumateTileDownloader, which takes over
    the network part of ShumateNetworkTileSource.
    
    ShumateRasterRenderer is a ShumateMapSource that provides tiles by
    reading image files from a ShumateDataSource. Similarly, ShumateVectorRenderer
    is a ShumateMapSource that provides tiles by rendering vector tiles.
    
    This commit also modifies the network source to use the tile URL as a
    file cache key, rather than the map source ID. This is because, with
    vector tiles, two different map sources may use the same set of tiles.

 .gitlab-ci.yml                                     |    2 +-
 demos/shumate-demo-window.c                        |   27 +-
 shumate/meson.build                                |   14 +-
 shumate/shumate-data-source.c                      |  129 +++
 shumate/shumate-data-source.h                      |   57 +
 shumate/shumate-file-cache.c                       |   86 +-
 shumate/shumate-file-cache.h                       |   46 +-
 shumate/shumate-map-source-registry.c              |   21 +-
 shumate/shumate-network-tile-source.c              | 1122 --------------------
 shumate/shumate-network-tile-source.h              |  115 --
 shumate/shumate-raster-renderer.c                  |  370 +++++++
 shumate/shumate-raster-renderer.h                  |   53 +
 shumate/shumate-tile-downloader.c                  |  510 +++++++++
 shumate/shumate-tile-downloader.h                  |   60 ++
 shumate/shumate-vector-renderer.c                  |  618 +++++++++++
 shumate/shumate-vector-renderer.h                  |   92 ++
 shumate/shumate-vector-style.c                     |  334 ------
 shumate/shumate-vector-style.h                     |   65 --
 shumate/shumate.h                                  |    6 +-
 shumate/vector/shumate-vector-expression-filter.c  |    2 +-
 .../vector/shumate-vector-expression-interpolate.c |    2 +-
 shumate/vector/shumate-vector-expression.c         |    2 +-
 shumate/vector/shumate-vector-utils-private.h      |    2 +-
 tests/file-cache.c                                 |   12 +-
 tests/license.c                                    |   42 +-
 tests/meson.build                                  |   18 +-
 tests/network-tile-source.c                        |  206 ----
 tests/test-tile-server.c                           |  153 ---
 tests/test-tile-server.h                           |   20 -
 tests/vector-style.c                               |    4 +-
 30 files changed, 2034 insertions(+), 2156 deletions(-)
---
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 032e99e..9c27e95 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -4,7 +4,7 @@ stages:
 
 fedora:
   stage: build
-  image: registry.gitlab.gnome.org/gnome/gtk/fedora:v30
+  image: registry.gitlab.gnome.org/gnome/gtk/fedora:v35
   script:
     - sudo dnf install -y vala sqlite-devel libsoup-devel gtk4-devel protobuf-c-devel
     - meson _build -Db_coverage=true -Dgtk_doc=true
diff --git a/demos/shumate-demo-window.c b/demos/shumate-demo-window.c
index 5afd5d7..051c640 100644
--- a/demos/shumate-demo-window.c
+++ b/demos/shumate-demo-window.c
@@ -129,7 +129,6 @@ shumate_demo_window_init (ShumateDemoWindow *self)
   GtkExpression *expression;
   g_autoptr(GBytes) bytes = NULL;
   const char *style_json;
-  g_autoptr(ShumateVectorStyle) style = NULL;
   GError *error = NULL;
 
   gtk_widget_init_template (GTK_WIDGET (self));
@@ -144,25 +143,25 @@ shumate_demo_window_init (ShumateDemoWindow *self)
   bytes = g_resources_lookup_data ("/org/gnome/Shumate/Demo/styles/map-style.json", 
G_RESOURCE_LOOKUP_FLAGS_NONE, NULL);
   style_json = g_bytes_get_data (bytes, NULL);
 
-  if (shumate_vector_style_is_supported ())
+  if (shumate_vector_renderer_is_supported ())
     {
-      if (!(style = shumate_vector_style_create (style_json, &error)))
+      ShumateVectorRenderer *renderer = shumate_vector_renderer_new_full_from_url (
+        "vector-tiles",
+        "Vector Tiles",
+        "© OpenStreetMap contributors", NULL, 0, 5, 512,
+        SHUMATE_MAP_PROJECTION_MERCATOR,
+        "https://jwestman.pages.gitlab.gnome.org/vector-tile-test-data/world_overview/#Z#/#X#/#Y#.pbf";,
+        style_json,
+        &error
+      );
+
+      if (error)
         {
           g_warning ("Failed to create vector map style: %s", error->message);
           g_clear_error (&error);
         }
       else
-        {
-          ShumateMapSource *map_source = SHUMATE_MAP_SOURCE (shumate_network_tile_source_new_vector_full (
-            "vector-tiles",
-            "Vector Tiles",
-            "© OpenStreetMap contributors", NULL, 0, 5, 512,
-            SHUMATE_MAP_PROJECTION_MERCATOR,
-            "https://jwestman.pages.gitlab.gnome.org/vector-tile-test-data/world_overview/#Z#/#X#/#Y#.pbf";,
-            style
-          ));
-          shumate_map_source_registry_add (self->registry, map_source);
-        }
+        shumate_map_source_registry_add (self->registry, SHUMATE_MAP_SOURCE (renderer));
     }
 
   viewport = shumate_map_get_viewport (self->map);
diff --git a/shumate/meson.build b/shumate/meson.build
index c8816a0..35de053 100644
--- a/shumate/meson.build
+++ b/shumate/meson.build
@@ -1,6 +1,7 @@
 libshumate_public_h = [
   'shumate-coordinate.h',
   'shumate-compass.h',
+  'shumate-data-source.h',
   'shumate-file-cache.h',
   'shumate-layer.h',
   'shumate-license.h',
@@ -12,12 +13,13 @@ libshumate_public_h = [
   'shumate-marker-layer.h',
   'shumate-marker.h',
   'shumate-memory-cache.h',
-  'shumate-network-tile-source.h',
   'shumate-path-layer.h',
   'shumate-point.h',
   'shumate-scale.h',
+  'shumate-raster-renderer.h',
   'shumate-tile.h',
-  'shumate-vector-style.h',
+  'shumate-tile-downloader.h',
+  'shumate-vector-renderer.h',
   'shumate-viewport.h',
   'shumate.h',
 ]
@@ -42,6 +44,7 @@ libshumate_private_h = [
 
 libshumate_sources = [
   'shumate-coordinate.c',
+  'shumate-data-source.c',
   'shumate-compass.c',
   'shumate-file-cache.c',
   'shumate-kinetic-scrolling.c',
@@ -55,12 +58,13 @@ libshumate_sources = [
   'shumate-marker-layer.c',
   'shumate-marker.c',
   'shumate-memory-cache.c',
-  'shumate-network-tile-source.c',
   'shumate-path-layer.c',
   'shumate-point.c',
   'shumate-scale.c',
+  'shumate-raster-renderer.c',
   'shumate-tile.c',
-  'shumate-vector-style.c',
+  'shumate-tile-downloader.c',
+  'shumate-vector-renderer.c',
   'shumate-viewport.c',
 ]
 
@@ -163,7 +167,7 @@ if get_option('vector_renderer')
   ]
 
   libshumate_c_args += [
-    '-DSHUMATE_VECTOR_RENDERER',
+    '-DSHUMATE_HAS_VECTOR_RENDERER',
   ]
 endif
 
diff --git a/shumate/shumate-data-source.c b/shumate/shumate-data-source.c
new file mode 100644
index 0000000..84c54d9
--- /dev/null
+++ b/shumate/shumate-data-source.c
@@ -0,0 +1,129 @@
+/*
+ * 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-data-source.h"
+
+G_DEFINE_TYPE (ShumateDataSource, shumate_data_source, G_TYPE_OBJECT)
+
+
+enum
+{
+  RECEIVED_DATA,
+  LAST_SIGNAL
+};
+static guint signals[LAST_SIGNAL] = { 0, };
+
+
+static GBytes *
+shumate_data_source_real_get_tile_data_finish (ShumateDataSource  *self,
+                                               GAsyncResult       *result,
+                                               GError            **error)
+{
+  g_return_val_if_fail (SHUMATE_IS_DATA_SOURCE (self), FALSE);
+  g_return_val_if_fail (g_task_is_valid (result, self), FALSE);
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+shumate_data_source_class_init (ShumateDataSourceClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  klass->get_tile_data_async = NULL;
+  klass->get_tile_data_finish = shumate_data_source_real_get_tile_data_finish;
+
+  /**
+   * ShumateDataSource::received-data:
+   * @self: the [class@DataSource] emitting the signal
+   * @x: the X coordinate of the tile
+   * @y: the Y coordinate of the tile
+   * @zoom_level: the zoom level of the tile
+   * @bytes: the received data
+   *
+   * Emitted when data is received for any tile. This includes any intermediate
+   * steps, such as data from the file cache, as well as the final result.
+   */
+  signals[RECEIVED_DATA] =
+    g_signal_new ("received-data",
+                  G_OBJECT_CLASS_TYPE (object_class),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL,
+                  NULL,
+                  G_TYPE_NONE,
+                  4, G_TYPE_INT, G_TYPE_INT, G_TYPE_INT, G_TYPE_BYTES);
+}
+
+static void
+shumate_data_source_init (ShumateDataSource *self)
+{
+}
+
+
+/**
+ * shumate_data_source_get_tile_data_async:
+ * @self: a [class@DataSource]
+ * @x: the X coordinate to fetch
+ * @y: the Y coordinate to fetch
+ * @zoom_level: the Z coordinate to fetch
+ * @cancellable: a #GCancellable
+ * @callback: a #GAsyncReadyCallback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Gets the data for the tile at the given coordinates.
+ *
+ * Some data sources may return data multiple times. For example,
+ * [class@TileDownloader] may return data from a cache, then return updated
+ * data from the network. [signal@received-data] is emitted each time
+ * data is received, then @callback is called after the last update.
+ */
+void
+shumate_data_source_get_tile_data_async (ShumateDataSource   *self,
+                                         int                  x,
+                                         int                  y,
+                                         int                  zoom_level,
+                                         GCancellable        *cancellable,
+                                         GAsyncReadyCallback  callback,
+                                         gpointer             user_data)
+{
+  g_return_if_fail (SHUMATE_IS_DATA_SOURCE (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  return SHUMATE_DATA_SOURCE_GET_CLASS (self)->get_tile_data_async (self, x, y, zoom_level, cancellable, 
callback, user_data);
+}
+
+
+/**
+ * shumate_data_source_get_tile_data_finish:
+ * @self: a [class@DataSource]
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Gets the final result of a request started with
+ * shumate_data_source_get_tile_data_async().
+ *
+ * Returns: (transfer full)(nullable): The requested data, or %NULL if an error occurred
+ */
+GBytes *
+shumate_data_source_get_tile_data_finish (ShumateDataSource  *self,
+                                          GAsyncResult       *result,
+                                          GError            **error)
+{
+  g_return_val_if_fail (SHUMATE_IS_DATA_SOURCE (self), FALSE);
+
+  return SHUMATE_DATA_SOURCE_GET_CLASS (self)->get_tile_data_finish (self, result, error);
+}
diff --git a/shumate/shumate-data-source.h b/shumate/shumate-data-source.h
new file mode 100644
index 0000000..f6540e3
--- /dev/null
+++ b/shumate/shumate-data-source.h
@@ -0,0 +1,57 @@
+/*
+ * 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 <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define SHUMATE_TYPE_DATA_SOURCE (shumate_data_source_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (ShumateDataSource, shumate_data_source, SHUMATE, DATA_SOURCE, GObject)
+
+struct _ShumateDataSourceClass
+{
+  GObjectClass parent_class;
+
+  void (*get_tile_data_async)     (ShumateDataSource   *self,
+                                   int                  x,
+                                   int                  y,
+                                   int                  zoom_level,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data);
+  GBytes *(*get_tile_data_finish) (ShumateDataSource  *self,
+                                   GAsyncResult       *result,
+                                   GError            **error);
+};
+
+
+void    shumate_data_source_get_tile_data_async (ShumateDataSource   *self,
+                                                 int                  x,
+                                                 int                  y,
+                                                 int                  zoom_level,
+                                                 GCancellable        *cancellable,
+                                                 GAsyncReadyCallback  callback,
+                                                 gpointer             user_data);
+GBytes *shumate_data_source_get_tile_data_finish (ShumateDataSource  *self,
+                                                  GAsyncResult       *result,
+                                                  GError            **error);
+
+G_END_DECLS
diff --git a/shumate/shumate-file-cache.c b/shumate/shumate-file-cache.c
index b5adee8..3818176 100644
--- a/shumate/shumate-file-cache.c
+++ b/shumate/shumate-file-cache.c
@@ -79,13 +79,17 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC (sqlite3_stmt, sqlite3_finalize);
 static void finalize_sql (ShumateFileCache *file_cache);
 static void init_cache (ShumateFileCache *file_cache);
 static char *get_filename (ShumateFileCache *file_cache,
-    ShumateTile *tile);
+                           int               x,
+                           int               y,
+                           int               zoom_level);
 static void delete_tile (ShumateFileCache *file_cache,
-    const char *filename);
+                         const char       *filename);
 static gboolean create_cache_dir (const char *dir_name);
 
 static void on_tile_filled (ShumateFileCache *self,
-    ShumateTile *tile);
+                            int               x,
+                            int               y,
+                            int               zoom_level);
 
 static void
 shumate_file_cache_get_property (GObject *object,
@@ -470,36 +474,37 @@ shumate_file_cache_set_size_limit (ShumateFileCache *file_cache,
 
 static char *
 get_filename (ShumateFileCache *file_cache,
-    ShumateTile *tile)
+              int               x,
+              int               y,
+              int               zoom_level)
 {
   ShumateFileCachePrivate *priv = shumate_file_cache_get_instance_private (file_cache);
   const char *cache_key;
 
   g_return_val_if_fail (SHUMATE_IS_FILE_CACHE (file_cache), NULL);
-  g_return_val_if_fail (SHUMATE_IS_TILE (tile), NULL);
   g_return_val_if_fail (priv->cache_dir, NULL);
 
   cache_key = shumate_file_cache_get_cache_key (file_cache);
 
   char *filename = g_strdup_printf ("%s" G_DIR_SEPARATOR_S
-        "%s" G_DIR_SEPARATOR_S
-        "%d" G_DIR_SEPARATOR_S
-        "%d" G_DIR_SEPARATOR_S "%d.png",
-        priv->cache_dir,
-        cache_key,
-        shumate_tile_get_zoom_level (tile),
-        shumate_tile_get_x (tile),
-        shumate_tile_get_y (tile));
+                                    "%s" G_DIR_SEPARATOR_S
+                                    "%d" G_DIR_SEPARATOR_S
+                                    "%d" G_DIR_SEPARATOR_S "%d.png",
+                                    priv->cache_dir,
+                                    cache_key,
+                                    zoom_level,
+                                    x,
+                                    y);
   return filename;
 }
 
 
 static char *
-db_get_etag (ShumateFileCache *self, ShumateTile *tile)
+db_get_etag (ShumateFileCache *self, int x, int y, int zoom_level)
 {
   ShumateFileCachePrivate *priv = shumate_file_cache_get_instance_private (self);
   int sql_rc = SQLITE_OK;
-  g_autofree char *filename = get_filename (self, tile);
+  g_autofree char *filename = get_filename (self, x, y, zoom_level);
 
   sqlite3_reset (priv->stmt_select);
   sql_rc = sqlite3_bind_text (priv->stmt_select, 1, filename, -1, SQLITE_STATIC);
@@ -534,7 +539,9 @@ db_get_etag (ShumateFileCache *self, ShumateTile *tile)
 /**
  * shumate_file_cache_mark_up_to_date:
  * @self: a #ShumateFileCache
- * @tile: a #ShumateTile
+ * @x: the X coordinate of the tile
+ * @y: the Y coordinate of the tile
+ * @zoom_level: the zoom level of the tile
  *
  * Marks a tile in the cache as being up to date, without changing its data.
  *
@@ -543,16 +550,17 @@ db_get_etag (ShumateFileCache *self, ShumateTile *tile)
  */
 void
 shumate_file_cache_mark_up_to_date (ShumateFileCache *self,
-                                    ShumateTile *tile)
+                                    int               x,
+                                    int               y,
+                                    int               zoom_level)
 {
   g_autofree char *filename = NULL;
   g_autoptr(GFile) file = NULL;
   g_autoptr(GFileInfo) info = NULL;
 
   g_return_if_fail (SHUMATE_IS_FILE_CACHE (self));
-  g_return_if_fail (SHUMATE_IS_TILE (tile));
 
-  filename = get_filename (self, tile);
+  filename = get_filename (self, x, y, zoom_level);
   file = g_file_new_for_path (filename);
 
   info = g_file_query_info (file, G_FILE_ATTRIBUTE_TIME_MODIFIED,
@@ -570,13 +578,13 @@ shumate_file_cache_mark_up_to_date (ShumateFileCache *self,
 
 static void
 on_tile_filled (ShumateFileCache *self,
-    ShumateTile *tile)
+                int x, int y, int zoom_level)
 {
   ShumateFileCachePrivate *priv = shumate_file_cache_get_instance_private (self);
   int sql_rc = SQLITE_OK;
   g_autofree char *filename = NULL;
 
-  filename = get_filename (self, tile);
+  filename = get_filename (self, x, y, zoom_level);
 
   g_debug ("popularity of %s", filename);
 
@@ -793,7 +801,9 @@ static void on_get_tile_file_loaded (GObject *source_object, GAsyncResult *res,
 /**
  * shumate_file_cache_get_tile_async:
  * @self: a #ShumateFileCache
- * @tile: a #ShumateTile
+ * @x: the X coordinate of the tile
+ * @y: the Y coordinate of the tile
+ * @zoom_level: the zoom level of the tile
  * @cancellable: (nullable): a #GCancellable
  * @callback: a #GAsyncReadyCallback to execute upon completion
  * @user_data: closure data for @callback
@@ -801,11 +811,13 @@ static void on_get_tile_file_loaded (GObject *source_object, GAsyncResult *res,
  * Gets tile data from the cache, if it is available.
  */
 void
-shumate_file_cache_get_tile_async (ShumateFileCache *self,
-                                   ShumateTile *tile,
-                                   GCancellable *cancellable,
-                                   GAsyncReadyCallback callback,
-                                   gpointer user_data)
+shumate_file_cache_get_tile_async (ShumateFileCache    *self,
+                                   int                  x,
+                                   int                  y,
+                                   int                  zoom_level,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
 {
   g_autoptr(GTask) task = NULL;
   g_autoptr(GFile) file = NULL;
@@ -815,7 +827,6 @@ shumate_file_cache_get_tile_async (ShumateFileCache *self,
   GetTileData *task_data = NULL;
 
   g_return_if_fail (SHUMATE_IS_FILE_CACHE (self));
-  g_return_if_fail (SHUMATE_IS_TILE (tile));
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   task = g_task_new (self, cancellable, callback, user_data);
@@ -824,7 +835,7 @@ shumate_file_cache_get_tile_async (ShumateFileCache *self,
   task_data = g_new0 (GetTileData, 1);
   g_task_set_task_data (task, task_data, (GDestroyNotify) get_tile_data_free);
 
-  filename = get_filename (self, tile);
+  filename = get_filename (self, x, y, zoom_level);
   file = g_file_new_for_path (filename);
 
   /* Retrieve modification time */
@@ -842,10 +853,10 @@ shumate_file_cache_get_tile_async (ShumateFileCache *self,
     }
 
   task_data->modtime = g_file_info_get_modification_date_time (info);
-  task_data->etag = db_get_etag (self, tile);
+  task_data->etag = db_get_etag (self, x, y, zoom_level);
 
   /* update tile popularity */
-  on_tile_filled (self, tile);
+  on_tile_filled (self, x, y, zoom_level);
 
   g_file_load_bytes_async (file, cancellable, on_get_tile_file_loaded, g_object_ref (task));
 }
@@ -940,7 +951,9 @@ static void on_file_written (GObject *object, GAsyncResult *result, gpointer use
 /**
  * shumate_file_cache_store_tile_async:
  * @self: an #ShumateFileCache
- * @tile: a #ShumateTile
+ * @x: the X coordinate of the tile
+ * @y: the Y coordinate of the tile
+ * @zoom_level: the zoom level of the tile
  * @bytes: a #GBytes
  * @etag: (nullable): an ETag string, or %NULL
  * @cancellable: (nullable): a #GCancellable
@@ -951,7 +964,9 @@ static void on_file_written (GObject *object, GAsyncResult *result, gpointer use
  */
 void
 shumate_file_cache_store_tile_async (ShumateFileCache *self,
-                                     ShumateTile *tile,
+                                     int x,
+                                     int y,
+                                     int zoom_level,
                                      GBytes *bytes,
                                      const char *etag,
                                      GCancellable *cancellable,
@@ -965,17 +980,16 @@ shumate_file_cache_store_tile_async (ShumateFileCache *self,
   StoreTileData *data;
 
   g_return_if_fail (SHUMATE_IS_FILE_CACHE (self));
-  g_return_if_fail (SHUMATE_IS_TILE (tile));
   g_return_if_fail (bytes != NULL);
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   task = g_task_new (self, cancellable, callback, user_data);
   g_task_set_source_tag (task, shumate_file_cache_store_tile_async);
 
-  filename = get_filename (self, tile);
+  filename = get_filename (self, x, y, zoom_level);
   file = g_file_new_for_path (filename);
 
-  g_debug ("Update of %p", tile);
+  g_debug ("Update of tile (%d %d zoom %d)", x, y, zoom_level);
 
   /* If needed, create the cache's dirs */
   path = g_path_get_dirname (filename);
diff --git a/shumate/shumate-file-cache.h b/shumate/shumate-file-cache.h
index bf55a4d..5c2c71b 100644
--- a/shumate/shumate-file-cache.h
+++ b/shumate/shumate-file-cache.h
@@ -26,7 +26,7 @@
 #define _SHUMATE_FILE_CACHE_H_
 
 #include <glib-object.h>
-#include <shumate/shumate-tile.h>
+#include <gio/gio.h>
 
 G_BEGIN_DECLS
 
@@ -77,30 +77,36 @@ gboolean shumate_file_cache_purge_cache_finish (ShumateFileCache *self,
                                                 GAsyncResult *result,
                                                 GError **error);
 
-void shumate_file_cache_get_tile_async (ShumateFileCache *self,
-                                        ShumateTile *tile,
-                                        GCancellable *cancellable,
-                                        GAsyncReadyCallback callback,
-                                        gpointer user_data);
-GBytes *shumate_file_cache_get_tile_finish (ShumateFileCache *self,
-                                            char **etag,
-                                            GDateTime **modtime,
-                                            GAsyncResult *result,
-                                            GError **error);
-
-void shumate_file_cache_store_tile_async (ShumateFileCache *self,
-                                          ShumateTile *tile,
-                                          GBytes *bytes,
-                                          const char *etag,
-                                          GCancellable *cancellable,
-                                          GAsyncReadyCallback callback,
-                                          gpointer user_data);
+void shumate_file_cache_get_tile_async (ShumateFileCache    *self,
+                                        int                  x,
+                                        int                  y,
+                                        int                  zoom_level,
+                                        GCancellable        *cancellable,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data);
+GBytes *shumate_file_cache_get_tile_finish (ShumateFileCache  *self,
+                                            char             **etag,
+                                            GDateTime        **modtime,
+                                            GAsyncResult      *result,
+                                            GError           **error);
+
+void shumate_file_cache_store_tile_async (ShumateFileCache    *self,
+                                          int                  x,
+                                          int                  y,
+                                          int                  zoom_level,
+                                          GBytes              *bytes,
+                                          const char          *etag,
+                                          GCancellable        *cancellable,
+                                          GAsyncReadyCallback  callback,
+                                          gpointer             user_data);
 gboolean shumate_file_cache_store_tile_finish (ShumateFileCache *self,
                                                GAsyncResult *result,
                                                GError **error);
 
 void shumate_file_cache_mark_up_to_date (ShumateFileCache *self,
-                                         ShumateTile *tile);
+                                         int               x,
+                                         int               y,
+                                         int               zoom_level);
 
 G_END_DECLS
 
diff --git a/shumate/shumate-map-source-registry.c b/shumate/shumate-map-source-registry.c
index 691767f..6c7d973 100644
--- a/shumate/shumate-map-source-registry.c
+++ b/shumate/shumate-map-source-registry.c
@@ -33,7 +33,8 @@
 
 #include <gio/gio.h>
 
-#include "shumate-network-tile-source.h"
+#include "shumate-raster-renderer.h"
+#include "shumate-tile-downloader.h"
 
 struct _ShumateMapSourceRegistry
 {
@@ -162,7 +163,7 @@ shumate_map_source_registry_populate_defaults (ShumateMapSourceRegistry *self)
   if (!shumate_map_source_registry_get_by_id (self, SHUMATE_MAP_SOURCE_OSM_MAPNIK))
     {
       g_ptr_array_add (self->map_sources,
-        shumate_network_tile_source_new_full (
+        shumate_raster_renderer_new_full_from_url (
           SHUMATE_MAP_SOURCE_OSM_MAPNIK,
           "OpenStreetMap Mapnik",
           "Map Data ODBL OpenStreetMap Contributors, Map Imagery CC-BY-SA 2.0 OpenStreetMap",
@@ -179,7 +180,7 @@ shumate_map_source_registry_populate_defaults (ShumateMapSourceRegistry *self)
   if (!shumate_map_source_registry_get_by_id (self, SHUMATE_MAP_SOURCE_OSM_CYCLE_MAP))
     {
       g_ptr_array_add (self->map_sources,
-        shumate_network_tile_source_new_full (
+        shumate_raster_renderer_new_full_from_url (
           SHUMATE_MAP_SOURCE_OSM_CYCLE_MAP,
           "OpenStreetMap Cycle Map",
           "Map data is CC-BY-SA 2.0 OpenStreetMap contributors",
@@ -196,7 +197,7 @@ shumate_map_source_registry_populate_defaults (ShumateMapSourceRegistry *self)
   if (!shumate_map_source_registry_get_by_id (self, SHUMATE_MAP_SOURCE_OSM_TRANSPORT_MAP))
     {
       g_ptr_array_add (self->map_sources,
-        shumate_network_tile_source_new_full (
+        shumate_raster_renderer_new_full_from_url (
           SHUMATE_MAP_SOURCE_OSM_TRANSPORT_MAP,
           "OpenStreetMap Transport Map",
           "Map data is CC-BY-SA 2.0 OpenStreetMap contributors",
@@ -213,7 +214,7 @@ shumate_map_source_registry_populate_defaults (ShumateMapSourceRegistry *self)
   if (!shumate_map_source_registry_get_by_id (self, SHUMATE_MAP_SOURCE_MFF_RELIEF))
     {
       g_ptr_array_add (self->map_sources,
-        shumate_network_tile_source_new_full (
+        shumate_raster_renderer_new_full_from_url (
           SHUMATE_MAP_SOURCE_MFF_RELIEF,
           "Maps for Free Relief",
           "Map data available under GNU Free Documentation license, Version 1.2 or later",
@@ -230,7 +231,7 @@ shumate_map_source_registry_populate_defaults (ShumateMapSourceRegistry *self)
   if (!shumate_map_source_registry_get_by_id (self, SHUMATE_MAP_SOURCE_OWM_CLOUDS))
     {
       g_ptr_array_add (self->map_sources,
-        shumate_network_tile_source_new_full (
+        shumate_raster_renderer_new_full_from_url (
           SHUMATE_MAP_SOURCE_OWM_CLOUDS,
           "OpenWeatherMap cloud layer",
           "Map data is CC-BY-SA 2.0 OpenWeatherMap contributors",
@@ -247,7 +248,7 @@ shumate_map_source_registry_populate_defaults (ShumateMapSourceRegistry *self)
   if (!shumate_map_source_registry_get_by_id (self, SHUMATE_MAP_SOURCE_OWM_WIND))
     {
       g_ptr_array_add (self->map_sources,
-        shumate_network_tile_source_new_full (
+        shumate_raster_renderer_new_full_from_url (
           SHUMATE_MAP_SOURCE_OWM_WIND,
           "OpenWeatherMap wind layer",
           "Map data is CC-BY-SA 2.0 OpenWeatherMap contributors",
@@ -264,7 +265,7 @@ shumate_map_source_registry_populate_defaults (ShumateMapSourceRegistry *self)
   if (!shumate_map_source_registry_get_by_id (self, SHUMATE_MAP_SOURCE_OWM_TEMPERATURE))
     {
       g_ptr_array_add (self->map_sources,
-        shumate_network_tile_source_new_full (
+        shumate_raster_renderer_new_full_from_url (
           SHUMATE_MAP_SOURCE_OWM_TEMPERATURE,
           "OpenWeatherMap temperature layer",
           "Map data is CC-BY-SA 2.0 OpenWeatherMap contributors",
@@ -281,7 +282,7 @@ shumate_map_source_registry_populate_defaults (ShumateMapSourceRegistry *self)
   if (!shumate_map_source_registry_get_by_id (self, SHUMATE_MAP_SOURCE_OWM_PRECIPITATION))
     {
       g_ptr_array_add (self->map_sources,
-        shumate_network_tile_source_new_full (
+        shumate_raster_renderer_new_full_from_url (
           SHUMATE_MAP_SOURCE_OWM_PRECIPITATION,
           "OpenWeatherMap precipitation layer",
           "Map data is CC-BY-SA 2.0 OpenWeatherMap contributors",
@@ -298,7 +299,7 @@ shumate_map_source_registry_populate_defaults (ShumateMapSourceRegistry *self)
   if (!shumate_map_source_registry_get_by_id (self, SHUMATE_MAP_SOURCE_OWM_PRESSURE))
     {
       g_ptr_array_add (self->map_sources,
-        shumate_network_tile_source_new_full (
+        shumate_raster_renderer_new_full_from_url (
           SHUMATE_MAP_SOURCE_OWM_PRESSURE,
           "OpenWeatherMap sea level pressure layer",
           "Map data is CC-BY-SA 2.0 OpenWeatherMap contributors",
diff --git a/shumate/shumate-raster-renderer.c b/shumate/shumate-raster-renderer.c
new file mode 100644
index 0000000..c16a4b5
--- /dev/null
+++ b/shumate/shumate-raster-renderer.c
@@ -0,0 +1,370 @@
+/*
+ * 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-data-source.h"
+#include "shumate-raster-renderer.h"
+#include "shumate-tile-downloader.h"
+
+
+struct _ShumateRasterRenderer
+{
+  ShumateMapSource parent_instance;
+
+  ShumateDataSource *data_source;
+
+  GPtrArray *tiles;
+};
+
+G_DEFINE_TYPE (ShumateRasterRenderer, shumate_raster_renderer, SHUMATE_TYPE_MAP_SOURCE)
+
+enum {
+  PROP_0,
+  PROP_DATA_SOURCE,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+
+/**
+ * shumate_raster_renderer_new:
+ * @data_source: a [class@DataSource] to provide tile image data
+ *
+ * Creates a new [class@RasterRenderer] that uses the given data source.
+ *
+ * Returns: (transfer full): a newly constructed [class@RasterRenderer]
+ */
+ShumateRasterRenderer *
+shumate_raster_renderer_new (ShumateDataSource *data_source)
+{
+  g_return_val_if_fail (SHUMATE_IS_DATA_SOURCE (data_source), NULL);
+  return g_object_new (SHUMATE_TYPE_RASTER_RENDERER,
+                       "data-source", data_source,
+                       NULL);
+}
+
+/**
+ * shumate_raster_renderer_new_from_url:
+ * @url_template: a URL template to fetch tiles from
+ *
+ * Creates a new [class@RasterRenderer] that fetches tiles from the given URL
+ * using a [class@TileDownloader] data source.
+ *
+ * Equivalent to:
+ *
+ * ```c
+ * g_autoptr(ShumateTileDownloader) source = shumate_tile_downloader_new (url_template);
+ * ShumateRasterRenderer *renderer = shumate_raster_renderer_new (source);
+ * ```
+ *
+ * Returns: (transfer full): a newly constructed [class@RasterRenderer]
+ */
+ShumateRasterRenderer *
+shumate_raster_renderer_new_from_url (const char *url_template)
+{
+  g_autoptr(ShumateDataSource) data_source = NULL;
+
+  g_return_val_if_fail (url_template != NULL, NULL);
+
+  data_source = SHUMATE_DATA_SOURCE (shumate_tile_downloader_new (url_template));
+  return shumate_raster_renderer_new (data_source);
+}
+
+
+/**
+ * shumate_raster_renderer_new_full:
+ * @id: the map source's id
+ * @name: the map source's name
+ * @license: the map source's license
+ * @license_uri: the map source's license URI
+ * @min_zoom: the map source's minimum zoom level
+ * @max_zoom: the map source's maximum zoom level
+ * @tile_size: the map source's tile size (in pixels)
+ * @projection: the map source's projection
+ * @data_source: a [class@DataSource] to provide tile image data
+ *
+ * Creates a new [class@RasterRenderer] with the given details and a data
+ * source.
+ *
+ * Returns: a newly constructed [class@RasterRenderer]
+ */
+ShumateRasterRenderer *
+shumate_raster_renderer_new_full (const char           *id,
+                                  const char           *name,
+                                  const char           *license,
+                                  const char           *license_uri,
+                                  guint                 min_zoom,
+                                  guint                 max_zoom,
+                                  guint                 tile_size,
+                                  ShumateMapProjection  projection,
+                                  ShumateDataSource    *data_source)
+{
+  g_return_val_if_fail (SHUMATE_IS_DATA_SOURCE (data_source), NULL);
+
+  return g_object_new (SHUMATE_TYPE_RASTER_RENDERER,
+                       "id", id,
+                       "name", name,
+                       "license", license,
+                       "license-uri", license_uri,
+                       "min-zoom-level", min_zoom,
+                       "max-zoom-level", max_zoom,
+                       "tile-size", tile_size,
+                       "projection", projection,
+                       "data-source", data_source,
+                       NULL);
+}
+
+
+/**
+ * shumate_raster_renderer_new_full_from_url:
+ * @id: the map source's id
+ * @name: the map source's name
+ * @license: the map source's license
+ * @license_uri: the map source's license URI
+ * @min_zoom: the map source's minimum zoom level
+ * @max_zoom: the map source's maximum zoom level
+ * @tile_size: the map source's tile size (in pixels)
+ * @projection: the map source's projection
+ * @url_template: a URL template to fetch tiles from
+ *
+ * Creates a new [class@RasterRenderer] with the given details and a data
+ * source.
+ *
+ * Returns: a newly constructed [class@RasterRenderer]
+ */
+ShumateRasterRenderer *
+shumate_raster_renderer_new_full_from_url (const char           *id,
+                                           const char           *name,
+                                           const char           *license,
+                                           const char           *license_uri,
+                                           guint                 min_zoom,
+                                           guint                 max_zoom,
+                                           guint                 tile_size,
+                                           ShumateMapProjection  projection,
+                                           const char           *url_template)
+{
+  g_autoptr(ShumateTileDownloader) data_source = NULL;
+
+  g_return_val_if_fail (url_template != NULL, NULL);
+
+  data_source = shumate_tile_downloader_new (url_template);
+
+  return g_object_new (SHUMATE_TYPE_RASTER_RENDERER,
+                       "id", id,
+                       "name", name,
+                       "license", license,
+                       "license-uri", license_uri,
+                       "min-zoom-level", min_zoom,
+                       "max-zoom-level", max_zoom,
+                       "tile-size", tile_size,
+                       "projection", projection,
+                       "data-source", data_source,
+                       NULL);
+}
+
+
+static void
+on_data_source_received_data (ShumateRasterRenderer *self,
+                              int                    x,
+                              int                    y,
+                              int                    zoom_level,
+                              GBytes                *bytes,
+                              ShumateDataSource     *data_source)
+{
+  int i;
+  ShumateTile *tile;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GdkPixbuf) pixbuf = NULL;
+  g_autoptr(GInputStream) stream = NULL;
+  g_autoptr(GdkTexture) texture = NULL;
+
+  g_assert (SHUMATE_IS_RASTER_RENDERER (self));
+  g_assert (SHUMATE_IS_DATA_SOURCE (data_source));
+
+  stream = g_memory_input_stream_new_from_bytes (bytes);
+  pixbuf = gdk_pixbuf_new_from_stream (stream, NULL, &error);
+  if (error)
+    {
+      g_warning ("Failed to create texture from tile data: %s", error->message);
+      return;
+    }
+
+  texture = gdk_texture_new_for_pixbuf (pixbuf);
+
+  for (i = 0; i < self->tiles->len; i ++)
+    {
+      tile = self->tiles->pdata[i];
+
+      if (shumate_tile_get_x (tile) == x
+          && shumate_tile_get_y (tile) == y
+          && shumate_tile_get_zoom_level (tile) == zoom_level)
+        shumate_tile_set_texture (tile, texture);
+    }
+}
+
+
+static void
+shumate_raster_renderer_constructed (GObject *object)
+{
+  ShumateRasterRenderer *self = SHUMATE_RASTER_RENDERER (object);
+
+  g_signal_connect_object (self->data_source, "received-data", (GCallback)on_data_source_received_data, 
self, G_CONNECT_SWAPPED);
+}
+
+static void
+shumate_raster_renderer_finalize (GObject *object)
+{
+  ShumateRasterRenderer *self = (ShumateRasterRenderer *)object;
+
+  g_clear_object (&self->data_source);
+  g_clear_pointer (&self->tiles, g_ptr_array_unref);
+
+  G_OBJECT_CLASS (shumate_raster_renderer_parent_class)->finalize (object);
+}
+
+static void
+shumate_raster_renderer_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  ShumateRasterRenderer *self = SHUMATE_RASTER_RENDERER (object);
+
+  switch (prop_id)
+    {
+    case PROP_DATA_SOURCE:
+      g_value_set_object (value, self->data_source);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+shumate_raster_renderer_set_property (GObject      *object,
+                                      guint         prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
+{
+  ShumateRasterRenderer *self = SHUMATE_RASTER_RENDERER (object);
+
+  switch (prop_id)
+    {
+    case PROP_DATA_SOURCE:
+      g_set_object (&self->data_source, g_value_get_object (value));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void shumate_raster_renderer_fill_tile_async (ShumateMapSource    *map_source,
+                                                     ShumateTile         *tile,
+                                                     GCancellable        *cancellable,
+                                                     GAsyncReadyCallback  callback,
+                                                     gpointer             user_data);
+
+static void
+shumate_raster_renderer_class_init (ShumateRasterRendererClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  ShumateMapSourceClass *map_source_class = SHUMATE_MAP_SOURCE_CLASS (klass);
+
+  object_class->constructed = shumate_raster_renderer_constructed;
+  object_class->finalize = shumate_raster_renderer_finalize;
+  object_class->get_property = shumate_raster_renderer_get_property;
+  object_class->set_property = shumate_raster_renderer_set_property;
+
+  map_source_class->fill_tile_async = shumate_raster_renderer_fill_tile_async;
+
+  /**
+   * ShumateRasterRenderer:data-source:
+   *
+   * The data source that provides image tiles to display. In most cases,
+   * a [class@TileDownloader] is sufficient.
+   */
+  properties[PROP_DATA_SOURCE] =
+    g_param_spec_object ("data-source",
+                         "Data source",
+                         "Data source",
+                         SHUMATE_TYPE_DATA_SOURCE,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+shumate_raster_renderer_init (ShumateRasterRenderer *self)
+{
+  self->tiles = g_ptr_array_new_full (0, g_object_unref);
+}
+
+
+static void on_data_source_done (GObject      *object,
+                                 GAsyncResult *res,
+                                 gpointer      user_data);
+
+static void
+shumate_raster_renderer_fill_tile_async (ShumateMapSource    *map_source,
+                                         ShumateTile         *tile,
+                                         GCancellable        *cancellable,
+                                         GAsyncReadyCallback  callback,
+                                         gpointer             user_data)
+{
+  ShumateRasterRenderer *self = (ShumateRasterRenderer *)map_source;
+  g_autoptr(GTask) task = NULL;
+
+  g_return_if_fail (SHUMATE_IS_RASTER_RENDERER (self));
+  g_return_if_fail (SHUMATE_IS_TILE (tile));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, shumate_raster_renderer_fill_tile_async);
+
+  g_task_set_task_data (task, g_object_ref (tile), (GDestroyNotify)g_object_unref);
+
+  g_ptr_array_add (self->tiles, g_object_ref (tile));
+
+  shumate_data_source_get_tile_data_async (self->data_source,
+                                           shumate_tile_get_x (tile),
+                                           shumate_tile_get_y (tile),
+                                           shumate_tile_get_zoom_level (tile),
+                                           cancellable,
+                                           on_data_source_done,
+                                           g_steal_pointer (&task));
+}
+
+static void
+on_data_source_done (GObject *object, GAsyncResult *res, gpointer user_data)
+{
+  ShumateDataSource *data_source = SHUMATE_DATA_SOURCE (object);
+  g_autoptr(GTask) task = G_TASK (user_data);
+  ShumateRasterRenderer *self = g_task_get_source_object (task);
+  ShumateTile *tile = g_task_get_task_data (task);
+  GError *error = NULL;
+
+  g_ptr_array_remove_fast (self->tiles, tile);
+
+  if (!shumate_data_source_get_tile_data_finish (data_source, res, &error))
+    g_task_return_error (task, error);
+  else
+    {
+      g_task_return_boolean (task, TRUE);
+      shumate_tile_set_state (tile, SHUMATE_STATE_DONE);
+    }
+}
diff --git a/shumate/shumate-raster-renderer.h b/shumate/shumate-raster-renderer.h
new file mode 100644
index 0000000..9ae8354
--- /dev/null
+++ b/shumate/shumate-raster-renderer.h
@@ -0,0 +1,53 @@
+/*
+ * 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/shumate-data-source.h>
+#include <shumate/shumate-map-source.h>
+
+G_BEGIN_DECLS
+
+
+#define SHUMATE_TYPE_RASTER_RENDERER (shumate_raster_renderer_get_type())
+G_DECLARE_FINAL_TYPE (ShumateRasterRenderer, shumate_raster_renderer, SHUMATE, RASTER_RENDERER, 
ShumateMapSource)
+
+
+ShumateRasterRenderer *shumate_raster_renderer_new (ShumateDataSource *data_source);
+ShumateRasterRenderer *shumate_raster_renderer_new_from_url (const char *url_template);
+
+ShumateRasterRenderer *shumate_raster_renderer_new_full (const char           *id,
+                                                         const char           *name,
+                                                         const char           *license,
+                                                         const char           *license_uri,
+                                                         guint                 min_zoom,
+                                                         guint                 max_zoom,
+                                                         guint                 tile_size,
+                                                         ShumateMapProjection  projection,
+                                                         ShumateDataSource    *data_source);
+ShumateRasterRenderer *shumate_raster_renderer_new_full_from_url (const char           *id,
+                                                                  const char           *name,
+                                                                  const char           *license,
+                                                                  const char           *license_uri,
+                                                                  guint                 min_zoom,
+                                                                  guint                 max_zoom,
+                                                                  guint                 tile_size,
+                                                                  ShumateMapProjection  projection,
+                                                                  const char           *url_template);
+
+G_END_DECLS
diff --git a/shumate/shumate-tile-downloader.c b/shumate/shumate-tile-downloader.c
new file mode 100644
index 0000000..ce81536
--- /dev/null
+++ b/shumate/shumate-tile-downloader.c
@@ -0,0 +1,510 @@
+/*
+ * 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 <libsoup/soup.h>
+#include "shumate-tile-downloader.h"
+#include "shumate-file-cache.h"
+
+struct _ShumateTileDownloader
+{
+  ShumateDataSource parent_instance;
+
+  char *url_template;
+
+  SoupSession *soup_session;
+  ShumateFileCache *cache;
+};
+
+G_DEFINE_TYPE (ShumateTileDownloader, shumate_tile_downloader, SHUMATE_TYPE_DATA_SOURCE)
+
+enum {
+  PROP_0,
+  PROP_URL_TEMPLATE,
+  N_PROPS
+};
+static GParamSpec *properties [N_PROPS];
+
+
+/* The osm.org tile set require us to use no more than 2 simultaneous
+ * connections so let that be the default.
+ */
+#define MAX_CONNS_DEFAULT 2
+
+
+/**
+ * shumate_tile_downloader_new:
+ * @url_template: a URL template to fetch tiles from
+ *
+ * Creates a new [class@TileDownloader] that fetches tiles from an API and
+ * caches them on disk.
+ *
+ * See [property@url-template] for the format of the URL template.
+ *
+ * Returns: (transfer full): a newly constructed [class@TileDownloader]
+ */
+ShumateTileDownloader *
+shumate_tile_downloader_new (const char *url_template)
+{
+  return g_object_new (SHUMATE_TYPE_TILE_DOWNLOADER,
+                       "url-template", url_template,
+                       NULL);
+}
+
+static void
+shumate_tile_downloader_constructed (GObject *object)
+{
+  ShumateTileDownloader *self = (ShumateTileDownloader *)object;
+  g_autofree char *cache_key = NULL;
+
+  cache_key = g_strcanon (g_strdup (self->url_template), 
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", '_');
+  self->cache = shumate_file_cache_new_full (100 * 1000 * 1000, cache_key, NULL);
+
+  G_OBJECT_CLASS (shumate_tile_downloader_parent_class)->constructed (object);
+}
+
+static void
+shumate_tile_downloader_finalize (GObject *object)
+{
+  ShumateTileDownloader *self = (ShumateTileDownloader *)object;
+
+  g_clear_pointer (&self->url_template, g_free);
+  g_clear_object (&self->soup_session);
+  g_clear_object (&self->cache);
+
+  G_OBJECT_CLASS (shumate_tile_downloader_parent_class)->finalize (object);
+}
+
+static void
+shumate_tile_downloader_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  ShumateTileDownloader *self = SHUMATE_TILE_DOWNLOADER (object);
+
+  switch (prop_id)
+    {
+    case PROP_URL_TEMPLATE:
+      g_value_set_string (value, self->url_template);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+shumate_tile_downloader_set_property (GObject      *object,
+                                      guint         prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
+{
+  ShumateTileDownloader *self = SHUMATE_TILE_DOWNLOADER (object);
+
+  switch (prop_id)
+    {
+    case PROP_URL_TEMPLATE:
+      self->url_template = g_strdup (g_value_get_string (value));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+
+static void
+get_tile_data_async (ShumateDataSource   *data_source,
+                     int                  x,
+                     int                  y,
+                     int                  zoom_level,
+                     GCancellable        *cancellable,
+                     GAsyncReadyCallback  callback,
+                     gpointer             user_data);
+
+static void
+shumate_tile_downloader_class_init (ShumateTileDownloaderClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  ShumateDataSourceClass *source_class = SHUMATE_DATA_SOURCE_CLASS (klass);
+
+  object_class->constructed = shumate_tile_downloader_constructed;
+  object_class->finalize = shumate_tile_downloader_finalize;
+  object_class->get_property = shumate_tile_downloader_get_property;
+  object_class->set_property = shumate_tile_downloader_set_property;
+
+  source_class->get_tile_data_async = get_tile_data_async;
+
+  /**
+   * ShumateTileDownloader:url-template:
+   *
+   * A template for construting the URL to download a tile from.
+   *
+   * The template has the following replacements:
+   * - "#X#": The X coordinate of the tile
+   * - "#Y#": The Y coordinate of the tile
+   * - "#Z#": The zoom level of the tile
+   * - "#TMSY#": The inverted Y coordinate (i.e. tile numbering starts with 0 at
+   * the bottom, rather than top, of the map)
+   */
+  properties[PROP_URL_TEMPLATE] =
+    g_param_spec_string ("url-template", "URL template", "URL template",
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+shumate_tile_downloader_init (ShumateTileDownloader *self)
+{
+}
+
+
+static void on_file_cache_get_tile (GObject *source_object, GAsyncResult *res, gpointer user_data);
+static void fetch_from_network (GTask *task);
+static void on_message_sent (GObject *source_object, GAsyncResult *res, gpointer user_data);
+static void on_message_read (GObject *source_object, GAsyncResult *res, gpointer user_data);
+
+static void on_file_cache_stored (GObject *source_object, GAsyncResult *res, gpointer user_data);
+
+typedef struct {
+  ShumateTileDownloader *self;
+  int x;
+  int y;
+  int z;
+  GBytes *bytes;
+  char *etag;
+  SoupMessage *msg;
+  GDateTime *modtime;
+} FillTileData;
+
+static void
+fill_tile_data_free (FillTileData *data)
+{
+  g_clear_object (&data->self);
+  g_clear_pointer (&data->bytes, g_bytes_unref);
+  g_clear_pointer (&data->etag, g_free);
+  g_clear_object (&data->msg);
+  g_clear_pointer (&data->modtime, g_date_time_unref);
+  g_free (data);
+}
+
+static gboolean
+tile_is_expired (GDateTime *modified_time)
+{
+  g_autoptr(GDateTime) now = g_date_time_new_now_utc ();
+  GTimeSpan diff = g_date_time_difference (now, modified_time);
+
+  return diff > 7 * G_TIME_SPAN_DAY; /* Cache expires in 7 days */
+}
+
+static char *
+get_modified_time_string (GDateTime *modified_time)
+{
+  if (modified_time == NULL)
+    return NULL;
+
+  return g_date_time_format (modified_time, "%a, %d %b %Y %T %Z");
+}
+
+
+#define SIZE 8
+static char *
+get_tile_uri (ShumateTileDownloader *self,
+              int                    x,
+              int                    y,
+              int                    z)
+{
+  char **tokens;
+  char *token;
+  GString *ret = NULL;
+  int i = 0;
+
+  tokens = g_strsplit (self->url_template, "#", 20);
+  token = tokens[i];
+  ret = g_string_sized_new (strlen (self->url_template));
+
+  while (token != NULL)
+    {
+      int number = G_MAXINT;
+      char value[SIZE];
+
+      if (strcmp (token, "X") == 0)
+        number = x;
+      if (strcmp (token, "Y") == 0)
+        number = y;
+      if (strcmp (token, "TMSY") == 0){
+        int ymax = 1 << z;
+        number = ymax - y - 1;
+      }
+      if (strcmp (token, "Z") == 0)
+        number = z;
+
+      if (number != G_MAXINT)
+        {
+          g_snprintf (value, SIZE, "%d", number);
+          g_string_append (ret, value);
+        }
+      else
+        g_string_append (ret, token);
+
+      token = tokens[++i];
+    }
+
+  token = ret->str;
+  g_strfreev (tokens);
+  g_string_free (ret, FALSE);
+
+  return token;
+}
+
+
+static void
+get_tile_data_async (ShumateDataSource   *data_source,
+                     int                  x,
+                     int                  y,
+                     int                  zoom_level,
+                     GCancellable        *cancellable,
+                     GAsyncReadyCallback  callback,
+                     gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  ShumateTileDownloader *self = (ShumateTileDownloader *)data_source;
+  FillTileData *data;
+
+  g_return_if_fail (SHUMATE_IS_TILE_DOWNLOADER (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, get_tile_data_async);
+
+  data = g_new0 (FillTileData, 1);
+  data->self = g_object_ref (self);
+  data->x = x;
+  data->y = y;
+  data->z = zoom_level;
+  g_task_set_task_data (task, data, (GDestroyNotify) fill_tile_data_free);
+
+  shumate_file_cache_get_tile_async (self->cache, x, y, zoom_level, cancellable, on_file_cache_get_tile, 
g_object_ref (task));
+}
+
+static void
+on_file_cache_get_tile (GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+  g_autoptr(GTask) task = user_data;
+  FillTileData *data = g_task_get_task_data (task);
+
+  data->bytes = shumate_file_cache_get_tile_finish (SHUMATE_FILE_CACHE (source_object),
+                                                    &data->etag, &data->modtime, res, NULL);
+
+  if (data->bytes != NULL)
+    {
+      g_signal_emit_by_name (data->self, "received-data", data->x, data->y, data->z, data->bytes);
+
+      if (!tile_is_expired (data->modtime))
+        {
+          g_task_return_pointer (task, g_steal_pointer (&data->bytes), (GDestroyNotify)g_bytes_unref);
+          return;
+        }
+    }
+
+  fetch_from_network (task);
+}
+
+static void
+fetch_from_network (GTask *task)
+{
+  FillTileData *data = g_task_get_task_data (task);
+  GCancellable *cancellable = g_task_get_cancellable (task);
+  g_autofree char *uri = NULL;
+  g_autofree char *modtime_string = NULL;
+
+  uri = get_tile_uri (data->self, data->x, data->y, data->z);
+
+  data->msg = soup_message_new (SOUP_METHOD_GET, uri);
+
+  if (data->msg == NULL)
+    {
+      g_task_return_new_error (task, SHUMATE_TILE_DOWNLOADER_ERROR,
+                               SHUMATE_TILE_DOWNLOADER_ERROR_MALFORMED_URL,
+                               "The URL %s is not valid", uri);
+      return;
+    }
+
+  modtime_string = get_modified_time_string (data->modtime);
+
+  /* If an etag is available, only use it.
+   * OSM servers seems to send now as the modified time for all tiles
+   * Omarender servers set the modified time correctly
+   */
+  if (data->etag)
+    {
+      g_debug ("If-None-Match: %s", data->etag);
+      soup_message_headers_append (data->msg->request_headers,
+          "If-None-Match", data->etag);
+    }
+  else if (modtime_string)
+    {
+      g_debug ("If-Modified-Since %s", modtime_string);
+      soup_message_headers_append (data->msg->request_headers,
+          "If-Modified-Since", modtime_string);
+    }
+
+  if (data->self->soup_session == NULL)
+    {
+      data->self->soup_session
+        = soup_session_new_with_options ("proxy-uri", NULL,
+                                         "ssl-strict", FALSE,
+                                         SOUP_SESSION_ADD_FEATURE_BY_TYPE,
+                                         SOUP_TYPE_PROXY_RESOLVER_DEFAULT,
+                                         SOUP_SESSION_ADD_FEATURE_BY_TYPE,
+                                         SOUP_TYPE_CONTENT_DECODER,
+                                         NULL);
+
+      g_object_set (G_OBJECT (data->self->soup_session),
+                    "user-agent",
+                    "libshumate/" SHUMATE_VERSION,
+                    "max-conns-per-host", MAX_CONNS_DEFAULT,
+                    "max-conns", MAX_CONNS_DEFAULT,
+                    NULL);
+    }
+
+  soup_session_send_async (data->self->soup_session, data->msg, cancellable, on_message_sent, g_object_ref 
(task));
+}
+
+/* Receive the response from the network. If the tile hasn't been modified,
+ * return; otherwise, read the data into a GBytes. */
+static void
+on_message_sent (GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+  g_autoptr(GTask) task = user_data;
+  FillTileData *data = g_task_get_task_data (task);
+  GCancellable *cancellable = g_task_get_cancellable (task);
+  g_autoptr(GInputStream) input_stream = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GOutputStream) output_stream = NULL;
+
+  input_stream = soup_session_send_finish (data->self->soup_session, res, &error);
+  if (error != NULL)
+    {
+      if (data->bytes)
+        {
+          /* The tile has already been filled from the cache, so the operation
+           * was overall successful even though the network request failed. */
+          g_debug ("Fetching tile failed, but there is a cached version (error: %s)", error->message);
+          g_task_return_pointer (task, g_steal_pointer (&data->bytes), (GDestroyNotify)g_bytes_unref);
+        }
+      else
+        g_task_return_error (task, g_steal_pointer (&error));
+
+      return;
+    }
+
+  g_debug ("Got reply %d", data->msg->status_code);
+
+  if (data->msg->status_code == SOUP_STATUS_NOT_MODIFIED)
+    {
+      /* The tile has already been filled from the cache, and the server says
+       * it doesn't have a newer one. Just update the cache, mark the tile as
+       * DONE, and return. */
+
+      shumate_file_cache_mark_up_to_date (data->self->cache, data->x, data->y, data->z);
+      g_task_return_pointer (task, g_steal_pointer (&data->bytes), (GDestroyNotify)g_bytes_unref);
+      return;
+    }
+
+  if (!SOUP_STATUS_IS_SUCCESSFUL (data->msg->status_code))
+    {
+      if (data->bytes)
+        {
+          g_debug ("Fetching tile failed, but there is a cached version (HTTP %s)",
+                   soup_status_get_phrase (data->msg->status_code));
+          g_task_return_pointer (task, g_steal_pointer (&data->bytes), (GDestroyNotify)g_bytes_unref);
+        }
+      else
+        {
+          g_task_return_new_error (task, SHUMATE_TILE_DOWNLOADER_ERROR,
+                                   SHUMATE_TILE_DOWNLOADER_ERROR_BAD_RESPONSE,
+                                   "Unable to download tile: HTTP %s",
+                                   soup_status_get_phrase (data->msg->status_code));
+        }
+
+      return;
+    }
+
+  /* Verify if the server sent an etag and save it */
+  g_clear_pointer (&data->etag, g_free);
+  data->etag = g_strdup (soup_message_headers_get_one (data->msg->response_headers, "ETag"));
+  g_debug ("Received ETag %s", data->etag);
+
+  output_stream = g_memory_output_stream_new_resizable ();
+  g_output_stream_splice_async (output_stream,
+                                input_stream,
+                                G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET,
+                                G_PRIORITY_DEFAULT,
+                                cancellable,
+                                on_message_read,
+                                g_steal_pointer (&task));
+}
+
+static void
+on_message_read (GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+  GOutputStream *output_stream = G_OUTPUT_STREAM (source_object);
+  g_autoptr(GTask) task = user_data;
+  GCancellable *cancellable = g_task_get_cancellable (task);
+  FillTileData *data = g_task_get_task_data (task);
+  g_autoptr(GError) error = NULL;
+
+  g_output_stream_splice_finish (output_stream, res, &error);
+  if (error != NULL)
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  g_bytes_unref (data->bytes);
+  data->bytes = g_memory_output_stream_steal_as_bytes (G_MEMORY_OUTPUT_STREAM (output_stream));
+
+  g_signal_emit_by_name (data->self, "received-data", data->x, data->y, data->z, data->bytes);
+
+  shumate_file_cache_store_tile_async (data->self->cache,
+                                       data->x, data->y, data->z,
+                                       data->bytes,
+                                       data->etag,
+                                       cancellable,
+                                       on_file_cache_stored,
+                                       g_steal_pointer (&task));
+}
+
+static void
+on_file_cache_stored (GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+  g_autoptr(GTask) task = user_data;
+  FillTileData *data = g_task_get_task_data (task);
+
+  g_task_return_pointer (task, g_steal_pointer (&data->bytes), (GDestroyNotify)g_bytes_unref);
+}
+
+
+/**
+ * shumate_tile_downloader_error_quark:
+ *
+ * Gets the #ShumateTileDownloader error quark.
+ *
+ * Returns: a #GQuark
+ */
+G_DEFINE_QUARK (shumate-tile-downloader-error-quark, shumate_tile_downloader_error);
diff --git a/shumate/shumate-tile-downloader.h b/shumate/shumate-tile-downloader.h
new file mode 100644
index 0000000..bf94c3a
--- /dev/null
+++ b/shumate/shumate-tile-downloader.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 <shumate/shumate-data-source.h>
+
+G_BEGIN_DECLS
+
+
+#define SHUMATE_TYPE_TILE_DOWNLOADER (shumate_tile_downloader_get_type())
+G_DECLARE_FINAL_TYPE (ShumateTileDownloader, shumate_tile_downloader, SHUMATE, TILE_DOWNLOADER, 
ShumateDataSource)
+
+ShumateTileDownloader *shumate_tile_downloader_new (const char *url_template);
+
+
+/**
+ * SHUMATE_TILE_DOWNLOADER_ERROR:
+ *
+ * Error domain for errors that may occur while fetching tiles from the network
+ * using [class@TileDownloader]. Errors in this domain will be from the
+ * [enum@TileDownloaderError] enum.
+ */
+#define SHUMATE_TILE_DOWNLOADER_ERROR shumate_tile_downloader_error_quark ()
+GQuark shumate_tile_downloader_error_quark (void);
+
+/**
+ * ShumateTileDownloaderError:
+ * @SHUMATE_TILE_DOWNLOADER_ERROR_FAILED: An unspecified error occurred during the operation.
+ * @SHUMATE_TILE_DOWNLOADER_ERROR_BAD_RESPONSE: An unsuccessful HTTP response was received from the server.
+ * @SHUMATE_TILE_DOWNLOADER_ERROR_COULD_NOT_CONNECT: The server could not be reached.
+ * @SHUMATE_TILE_DOWNLOADER_ERROR_MALFORMED_URL: The provided URL isn't valid
+ * @SHUMATE_TILE_DOWNLOADER_ERROR_OFFLINE: The tile source has been marked as offline.
+ *
+ * Error codes in the #SHUMATE_TILE_DOWNLOADER_ERROR domain.
+ */
+typedef enum {
+  SHUMATE_TILE_DOWNLOADER_ERROR_FAILED,
+  SHUMATE_TILE_DOWNLOADER_ERROR_BAD_RESPONSE,
+  SHUMATE_TILE_DOWNLOADER_ERROR_COULD_NOT_CONNECT,
+  SHUMATE_TILE_DOWNLOADER_ERROR_MALFORMED_URL,
+  SHUMATE_TILE_DOWNLOADER_ERROR_OFFLINE,
+} ShumateTileDownloaderError;
+
+
+G_END_DECLS
diff --git a/shumate/shumate-vector-renderer.c b/shumate/shumate-vector-renderer.c
new file mode 100644
index 0000000..c376aaa
--- /dev/null
+++ b/shumate/shumate-vector-renderer.c
@@ -0,0 +1,618 @@
+/*
+ * 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-renderer.h"
+#include "shumate-tile-downloader.h"
+
+#ifdef SHUMATE_HAS_VECTOR_RENDERER
+#include <json-glib/json-glib.h>
+#include <cairo/cairo.h>
+
+#include "vector/shumate-vector-render-scope-private.h"
+#include "vector/shumate-vector-utils-private.h"
+#include "vector/shumate-vector-layer-private.h"
+#endif
+
+struct _ShumateVectorRenderer
+{
+  ShumateMapSource parent_instance;
+
+  ShumateDataSource *data_source;
+  GPtrArray *tiles;
+
+  char *style_json;
+
+  GPtrArray *layers;
+};
+
+static void shumate_vector_renderer_initable_iface_init (GInitableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (ShumateVectorRenderer, shumate_vector_renderer, SHUMATE_TYPE_MAP_SOURCE,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, 
shumate_vector_renderer_initable_iface_init))
+
+enum {
+  PROP_0,
+  PROP_DATA_SOURCE,
+  PROP_STYLE_JSON,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+
+/**
+ * shumate_vector_renderer_new:
+ * @data_source: a [class@DataSource] to provide tile image data
+ * @style_json: a vector style
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new [class@VectorRenderer] to render vector tiles from @data_source.
+ *
+ * Returns: (transfer full): a newly constructed [class@VectorRenderer]
+ */
+ShumateVectorRenderer *
+shumate_vector_renderer_new (ShumateDataSource  *data_source,
+                             const char         *style_json,
+                             GError            **error)
+{
+  g_return_val_if_fail (SHUMATE_IS_DATA_SOURCE (data_source), NULL);
+  return g_initable_new (SHUMATE_TYPE_VECTOR_RENDERER, NULL, error,
+                         "data-source", data_source,
+                         "style-json", style_json,
+                         NULL);
+}
+
+
+/**
+ * shumate_vector_renderer_new_from_url:
+ * @url_template: a URL template to fetch tiles from
+ * @style_json: a vector style
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new [class@VectorRenderer] that fetches tiles from the given URL
+ * using a [class@TileDownloader] data source.
+ *
+ * Equivalent to:
+ *
+ * ```c
+ * g_autoptr(ShumateTileDownloader) source = shumate_tile_downloader_new (url_template);
+ * ShumateVectorRenderer *renderer = shumate_vector_renderer_new (source);
+ * ```
+ *
+ * Returns: (transfer full): a newly constructed [class@VectorRenderer]
+ */
+ShumateVectorRenderer *
+shumate_vector_renderer_new_from_url (const char  *url_template,
+                                      const char  *style_json,
+                                      GError     **error)
+{
+  g_autoptr(ShumateDataSource) data_source = NULL;
+
+  g_return_val_if_fail (url_template != NULL, NULL);
+
+  data_source = SHUMATE_DATA_SOURCE (shumate_tile_downloader_new (url_template));
+  return shumate_vector_renderer_new (data_source, style_json, error);
+}
+
+
+/**
+ * shumate_vector_renderer_new_full:
+ * @id: the map source's id
+ * @name: the map source's name
+ * @license: the map source's license
+ * @license_uri: the map source's license URI
+ * @min_zoom: the map source's minimum zoom level
+ * @max_zoom: the map source's maximum zoom level
+ * @tile_size: the map source's tile size (in pixels)
+ * @projection: the map source's projection
+ * @data_source: a [class@DataSource] to provide tile image data
+ * @error: (nullable): a return location for a #GError, or %NULL
+ *
+ * Creates a new [class@VectorRenderer] to render vector tiles from @data_source.
+ *
+ * Returns: a newly constructed [class@VectorRenderer] object
+ */
+ShumateVectorRenderer *
+shumate_vector_renderer_new_full (const char            *id,
+                                  const char            *name,
+                                  const char            *license,
+                                  const char            *license_uri,
+                                  guint                  min_zoom,
+                                  guint                  max_zoom,
+                                  guint                  tile_size,
+                                  ShumateMapProjection   projection,
+                                  ShumateDataSource     *data_source,
+                                  const char            *style_json,
+                                  GError               **error)
+{
+  g_return_val_if_fail (SHUMATE_IS_DATA_SOURCE (data_source), NULL);
+
+  return g_initable_new (SHUMATE_TYPE_VECTOR_RENDERER, NULL, error,
+                         "id", id,
+                         "name", name,
+                         "license", license,
+                         "license-uri", license_uri,
+                         "min-zoom-level", min_zoom,
+                         "max-zoom-level", max_zoom,
+                         "tile-size", tile_size,
+                         "projection", projection,
+                         "data-source", data_source,
+                         "style-json", style_json,
+                         NULL);
+}
+
+
+/**
+ * shumate_vector_renderer_new_full_from_url:
+ * @id: the map source's id
+ * @name: the map source's name
+ * @license: the map source's license
+ * @license_uri: the map source's license URI
+ * @min_zoom: the map source's minimum zoom level
+ * @max_zoom: the map source's maximum zoom level
+ * @tile_size: the map source's tile size (in pixels)
+ * @projection: the map source's projection
+ * @url_template: a template for the URL to fetch tiles from
+ * @error: (nullable): a return location for a #GError, or %NULL
+ *
+ * Creates a new [class@VectorRenderer] that fetches tiles from the given URL
+ * using a [class@TileDownloader] data source.
+ *
+ * Returns: a newly constructed [class@VectorRenderer] object
+ */
+ShumateVectorRenderer *
+shumate_vector_renderer_new_full_from_url (const char            *id,
+                                           const char            *name,
+                                           const char            *license,
+                                           const char            *license_uri,
+                                           guint                  min_zoom,
+                                           guint                  max_zoom,
+                                           guint                  tile_size,
+                                           ShumateMapProjection   projection,
+                                           const char            *url_template,
+                                           const char            *style_json,
+                                           GError               **error)
+{
+  g_autoptr(ShumateTileDownloader) data_source = NULL;
+
+  g_return_val_if_fail (url_template != NULL, NULL);
+
+  data_source = shumate_tile_downloader_new (url_template);
+
+  return g_initable_new (SHUMATE_TYPE_VECTOR_RENDERER, NULL, error,
+                         "id", id,
+                         "name", name,
+                         "license", license,
+                         "license-uri", license_uri,
+                         "min-zoom-level", min_zoom,
+                         "max-zoom-level", max_zoom,
+                         "tile-size", tile_size,
+                         "projection", projection,
+                         "data-source", data_source,
+                         "style-json", style_json,
+                         NULL);
+}
+
+
+/**
+ * shumate_vector_renderer_is_supported:
+ *
+ * Checks whether libshumate was compiled with vector tile support. If it was
+ * not, vector renderers cannot be created or used.
+ *
+ * Returns: %TRUE if libshumate was compiled with `-Dvector_renderer=true` or
+ * %FALSE if it was not
+ */
+gboolean
+shumate_vector_renderer_is_supported (void)
+{
+#ifdef SHUMATE_HAS_VECTOR_RENDERER
+  return TRUE;
+#else
+  return FALSE;
+#endif
+}
+
+
+static void
+on_data_source_received_data (ShumateVectorRenderer *self,
+                              int                    x,
+                              int                    y,
+                              int                    zoom_level,
+                              GBytes                *bytes,
+                              ShumateDataSource     *data_source);
+
+
+static void
+shumate_vector_renderer_constructed (GObject *object)
+{
+  ShumateVectorRenderer *self = SHUMATE_VECTOR_RENDERER (object);
+
+  g_signal_connect_object (self->data_source, "received-data", (GCallback)on_data_source_received_data, 
self, G_CONNECT_SWAPPED);
+}
+
+
+static void
+shumate_vector_renderer_finalize (GObject *object)
+{
+  ShumateVectorRenderer *self = (ShumateVectorRenderer *)object;
+
+  g_clear_pointer (&self->layers, g_ptr_array_unref);
+  g_clear_pointer (&self->style_json, g_free);
+  g_clear_object (&self->data_source);
+  g_clear_pointer (&self->tiles, g_ptr_array_unref);
+
+  G_OBJECT_CLASS (shumate_vector_renderer_parent_class)->finalize (object);
+}
+
+static void
+shumate_vector_renderer_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  ShumateVectorRenderer *self = SHUMATE_VECTOR_RENDERER (object);
+
+  switch (prop_id)
+    {
+    case PROP_DATA_SOURCE:
+      g_value_set_object (value, self->data_source);
+      break;
+    case PROP_STYLE_JSON:
+      g_value_set_string (value, self->style_json);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+shumate_vector_renderer_set_property (GObject      *object,
+                                      guint         prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
+{
+  ShumateVectorRenderer *self = SHUMATE_VECTOR_RENDERER (object);
+
+  switch (prop_id)
+    {
+    case PROP_DATA_SOURCE:
+      g_set_object (&self->data_source, g_value_get_object (value));
+      break;
+    case PROP_STYLE_JSON:
+      /* Property is construct only, so it should only be set once */
+      g_assert (self->style_json == NULL);
+      self->style_json = g_strdup (g_value_get_string (value));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void shumate_vector_renderer_fill_tile_async (ShumateMapSource    *map_source,
+                                                     ShumateTile         *tile,
+                                                     GCancellable        *cancellable,
+                                                     GAsyncReadyCallback  callback,
+                                                     gpointer             user_data);
+
+static void
+shumate_vector_renderer_class_init (ShumateVectorRendererClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  ShumateMapSourceClass *map_source_class = SHUMATE_MAP_SOURCE_CLASS (klass);
+
+  object_class->constructed = shumate_vector_renderer_constructed;
+  object_class->finalize = shumate_vector_renderer_finalize;
+  object_class->get_property = shumate_vector_renderer_get_property;
+  object_class->set_property = shumate_vector_renderer_set_property;
+
+  map_source_class->fill_tile_async = shumate_vector_renderer_fill_tile_async;
+
+  /**
+   * ShumateVectorRenderer:data-source:
+   *
+   * The data source that provides image tiles to display. In most cases,
+   * a [class@TileDownloader] is sufficient.
+   */
+  properties[PROP_DATA_SOURCE] =
+    g_param_spec_object ("data-source",
+                         "Data source",
+                         "Data source",
+                         SHUMATE_TYPE_DATA_SOURCE,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * ShumateVectorRenderer:style-json:
+   *
+   * A map style, in [Mapbox Style Specification](https://docs.mapbox.com/mapbox-gl-js/style-spec/)
+   * format.
+   *
+   * Note that not all features of the specification are supported.
+   */
+  properties[PROP_STYLE_JSON] =
+    g_param_spec_string ("style-json",
+                         "Style JSON",
+                         "Style JSON",
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | 
G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+
+static gboolean
+shumate_vector_renderer_initable_init (GInitable     *initable,
+                                       GCancellable  *cancellable,
+                                       GError       **error)
+{
+#ifdef SHUMATE_HAS_VECTOR_RENDERER
+  ShumateVectorRenderer *self = (ShumateVectorRenderer *)initable;
+  g_autoptr(JsonNode) node = NULL;
+  JsonNode *layers_node;
+  JsonObject *object;
+
+  g_return_val_if_fail (SHUMATE_IS_VECTOR_RENDERER (self), FALSE);
+  g_return_val_if_fail (self->style_json != NULL, FALSE);
+
+  if (!(node = json_from_string (self->style_json, error)))
+    return FALSE;
+
+  if (!shumate_vector_json_get_object (node, &object, error))
+    return FALSE;
+
+  self->layers = g_ptr_array_new_with_free_func (g_object_unref);
+  if ((layers_node = json_object_get_member (object, "layers")))
+    {
+      JsonArray *layers;
+
+      if (!shumate_vector_json_get_array (layers_node, &layers, error))
+        return FALSE;
+
+      for (int i = 0, n = json_array_get_length (layers); i < n; i ++)
+        {
+          JsonNode *layer_node = json_array_get_element (layers, i);
+          JsonObject *layer_obj;
+          ShumateVectorLayer *layer;
+
+          if (!shumate_vector_json_get_object (layer_node, &layer_obj, error))
+            return FALSE;
+
+          if (!(layer = shumate_vector_layer_create_from_json (layer_obj, error)))
+            {
+              g_prefix_error (error, "layer '%s': ", json_object_get_string_member (layer_obj, "id"));
+              return FALSE;
+            }
+
+          g_ptr_array_add (self->layers, layer);
+        }
+    }
+
+  return TRUE;
+#else
+  g_set_error (error,
+               SHUMATE_STYLE_ERROR,
+               SHUMATE_STYLE_ERROR_SUPPORT_OMITTED,
+               "Libshumate was compiled without support for vector tiles, so a "
+               "ShumateVectorRenderer may not be constructed. You can fix this "
+               "by compiling libshumate with `-Dvector_renderer=true` or by "
+               "checking `shumate_vector_renderer_is_supported ()` before trying "
+               "to construct a ShumateVectorRenderer.");
+  return FALSE;
+#endif
+}
+
+
+static void
+shumate_vector_renderer_initable_iface_init (GInitableIface *iface)
+{
+  iface->init = shumate_vector_renderer_initable_init;
+}
+
+
+static void
+shumate_vector_renderer_init (ShumateVectorRenderer *self)
+{
+  self->tiles = g_ptr_array_new_full (0, g_object_unref);
+}
+
+
+/**
+ * shumate_vector_renderer_get_style_json:
+ * @self: a [class@VectorStyle]
+ *
+ * Gets the JSON string from which this vector style was loaded.
+ *
+ * Returns: (nullable): the style JSON, or %NULL if none is set
+ */
+const char *
+shumate_vector_renderer_get_style_json (ShumateVectorRenderer *self)
+{
+  g_return_val_if_fail (SHUMATE_IS_VECTOR_RENDERER (self), NULL);
+
+  return self->style_json;
+}
+
+
+#ifdef SHUMATE_HAS_VECTOR_RENDERER
+static GdkTexture *
+texture_new_for_surface (cairo_surface_t *surface)
+{
+  g_autoptr(GBytes) bytes = NULL;
+  GdkTexture *texture;
+
+  g_return_val_if_fail (cairo_surface_get_type (surface) == CAIRO_SURFACE_TYPE_IMAGE, NULL);
+  g_return_val_if_fail (cairo_image_surface_get_width (surface) > 0, NULL);
+  g_return_val_if_fail (cairo_image_surface_get_height (surface) > 0, NULL);
+
+  bytes = g_bytes_new_with_free_func (cairo_image_surface_get_data (surface),
+                                      (gsize) cairo_image_surface_get_height (surface)
+                                      * (gsize) cairo_image_surface_get_stride (surface),
+                                      (GDestroyNotify) cairo_surface_destroy,
+                                      cairo_surface_reference (surface));
+
+  texture = gdk_memory_texture_new (cairo_image_surface_get_width (surface),
+                                    cairo_image_surface_get_height (surface),
+                                    GDK_MEMORY_B8G8R8A8_PREMULTIPLIED,
+                                    bytes,
+                                    cairo_image_surface_get_stride (surface));
+
+  return texture;
+}
+#endif
+
+
+static void on_data_source_done (GObject      *object,
+                                 GAsyncResult *res,
+                                 gpointer      user_data);
+
+typedef struct {
+  ShumateVectorRenderer *self;
+  ShumateTile *tile;
+} FillTileData;
+
+static void
+fill_tile_data_free (FillTileData *data)
+{
+  g_clear_object (&data->self);
+  g_clear_object (&data->tile);
+  g_free (data);
+}
+
+static void
+shumate_vector_renderer_fill_tile_async (ShumateMapSource    *map_source,
+                                         ShumateTile         *tile,
+                                         GCancellable        *cancellable,
+                                         GAsyncReadyCallback  callback,
+                                         gpointer             user_data)
+{
+  ShumateVectorRenderer *self = (ShumateVectorRenderer *)map_source;
+  g_autoptr(GTask) task = NULL;
+  FillTileData *data;
+
+  g_return_if_fail (SHUMATE_IS_VECTOR_RENDERER (self));
+  g_return_if_fail (SHUMATE_IS_TILE (tile));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, shumate_vector_renderer_fill_tile_async);
+
+  data = g_new0 (FillTileData, 1);
+  data->self = g_object_ref (self);
+  data->tile = g_object_ref (tile);
+  g_task_set_task_data (task, data, (GDestroyNotify)fill_tile_data_free);
+
+  g_ptr_array_add (self->tiles, g_object_ref (tile));
+
+  shumate_data_source_get_tile_data_async (self->data_source,
+                                           shumate_tile_get_x (tile),
+                                           shumate_tile_get_y (tile),
+                                           shumate_tile_get_zoom_level (tile),
+                                           cancellable,
+                                           on_data_source_done,
+                                           g_steal_pointer (&task));
+}
+
+static void
+on_data_source_done (GObject *object, GAsyncResult *res, gpointer user_data)
+{
+  ShumateDataSource *data_source = SHUMATE_DATA_SOURCE (object);
+  g_autoptr(GTask) task = G_TASK (user_data);
+  FillTileData *data = g_task_get_task_data (task);
+  GError *error = NULL;
+
+  g_ptr_array_remove_fast (data->self->tiles, data->tile);
+
+  if (!shumate_data_source_get_tile_data_finish (data_source, res, &error))
+    g_task_return_error (task, error);
+  else
+    {
+      g_task_return_boolean (task, TRUE);
+      shumate_tile_set_state (data->tile, SHUMATE_STATE_DONE);
+    }
+}
+
+static GdkTexture *
+render (ShumateVectorRenderer *self, int texture_size, GBytes *tile_data, double zoom_level)
+{
+#ifdef SHUMATE_HAS_VECTOR_RENDERER
+  ShumateVectorRenderScope scope;
+  GdkTexture *texture;
+  cairo_surface_t *surface;
+  gconstpointer data;
+  gsize len;
+
+  g_return_val_if_fail (SHUMATE_IS_VECTOR_RENDERER (self), NULL);
+
+  scope.target_size = texture_size;
+  scope.zoom_level = zoom_level;
+
+  surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, texture_size, texture_size);
+  scope.cr = cairo_create (surface);
+
+  data = g_bytes_get_data (tile_data, &len);
+  scope.tile = vector_tile__tile__unpack (NULL, len, data);
+
+  if (scope.tile != NULL)
+    for (int i = 0; i < self->layers->len; i ++)
+      shumate_vector_layer_render ((ShumateVectorLayer *)self->layers->pdata[i], &scope);
+
+  texture = texture_new_for_surface (surface);
+
+  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);
+#endif
+}
+
+static void
+on_data_source_received_data (ShumateVectorRenderer *self,
+                              int                    x,
+                              int                    y,
+                              int                    zoom_level,
+                              GBytes                *bytes,
+                              ShumateDataSource     *data_source)
+{
+  int i;
+  ShumateTile *tile;
+
+  g_assert (SHUMATE_IS_VECTOR_RENDERER (self));
+  g_assert (SHUMATE_IS_DATA_SOURCE (data_source));
+
+  for (i = 0; i < self->tiles->len; i ++)
+    {
+      tile = self->tiles->pdata[i];
+
+      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);
+        }
+    }
+}
+
+/**
+ * shumate_style_error_quark:
+ *
+ * Returns: a #GQuark
+ */
+G_DEFINE_QUARK (shumate-style-error-quark, shumate_style_error);
diff --git a/shumate/shumate-vector-renderer.h b/shumate/shumate-vector-renderer.h
new file mode 100644
index 0000000..9cbde4e
--- /dev/null
+++ b/shumate/shumate-vector-renderer.h
@@ -0,0 +1,92 @@
+/*
+ * 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/shumate-data-source.h>
+#include <shumate/shumate-map-source.h>
+
+G_BEGIN_DECLS
+
+
+#define SHUMATE_TYPE_VECTOR_RENDERER (shumate_vector_renderer_get_type())
+G_DECLARE_FINAL_TYPE (ShumateVectorRenderer, shumate_vector_renderer, SHUMATE, VECTOR_RENDERER, 
ShumateMapSource)
+
+
+gboolean shumate_vector_renderer_is_supported (void);
+
+
+ShumateVectorRenderer *shumate_vector_renderer_new (ShumateDataSource  *data_source,
+                                                    const char         *style_json,
+                                                    GError            **error);
+ShumateVectorRenderer *shumate_vector_renderer_new_from_url (const char  *url_template,
+                                                             const char  *style_json,
+                                                             GError     **error);
+
+ShumateVectorRenderer *shumate_vector_renderer_new_full (const char            *id,
+                                                         const char            *name,
+                                                         const char            *license,
+                                                         const char            *license_uri,
+                                                         guint                  min_zoom,
+                                                         guint                  max_zoom,
+                                                         guint                  tile_size,
+                                                         ShumateMapProjection   projection,
+                                                         ShumateDataSource     *data_source,
+                                                         const char            *style_json,
+                                                         GError               **error);
+ShumateVectorRenderer *shumate_vector_renderer_new_full_from_url (const char            *id,
+                                                                  const char            *name,
+                                                                  const char            *license,
+                                                                  const char            *license_uri,
+                                                                  guint                  min_zoom,
+                                                                  guint                  max_zoom,
+                                                                  guint                  tile_size,
+                                                                  ShumateMapProjection   projection,
+                                                                  const char            *url_template,
+                                                                  const char            *style_json,
+                                                                  GError               **error);
+
+
+/**
+ * SHUMATE_STYLE_ERROR:
+ *
+ * Error domain for errors that may occur when parsing a map style. Errors in
+ * this domain will be from the [enum@StyleError] enum.
+ */
+#define SHUMATE_STYLE_ERROR shumate_style_error_quark ()
+GQuark shumate_style_error_quark (void);
+
+/**
+ * ShumateStyleError:
+ * @SHUMATE_STYLE_ERROR_FAILED: An unspecified error occurred during the operation.
+ * @SHUMATE_STYLE_ERROR_MALFORMED_STYLE: A JSON node in the style has the wrong type (e.g. an object where 
there should be an array).
+ * @SHUMATE_STYLE_ERROR_UNSUPPORTED_LAYER: An unsupported layer type was encountered.
+ * @SHUMATE_STYLE_ERROR_INVALID_EXPRESSION: An invalid or unrecognized expression was encountered.
+ * @SHUMATE_STYLE_ERROR_SUPPORT_OMITTED: Libshumate was compiled without vector tile support.
+ *
+ * Error codes in the [error@StyleError] domain.
+ */
+typedef enum {
+  SHUMATE_STYLE_ERROR_FAILED,
+  SHUMATE_STYLE_ERROR_MALFORMED_STYLE,
+  SHUMATE_STYLE_ERROR_UNSUPPORTED_LAYER,
+  SHUMATE_STYLE_ERROR_INVALID_EXPRESSION,
+  SHUMATE_STYLE_ERROR_SUPPORT_OMITTED,
+} ShumateStyleError;
+
+G_END_DECLS
diff --git a/shumate/shumate.h b/shumate/shumate.h
index 2809970..d521dcc 100644
--- a/shumate/shumate.h
+++ b/shumate/shumate.h
@@ -28,6 +28,7 @@
 #include "shumate/shumate-enum-types.h"
 #include "shumate/shumate-version.h"
 
+#include "shumate/shumate-data-source.h"
 #include "shumate/shumate-license.h"
 #include "shumate/shumate-layer.h"
 #include "shumate/shumate-map-layer.h"
@@ -39,16 +40,17 @@
 #include "shumate/shumate-compass.h"
 #include "shumate/shumate-marker.h"
 #include "shumate/shumate-map.h"
+#include "shumate/shumate-raster-renderer.h"
 #include "shumate/shumate-viewport.h"
 #include "shumate/shumate-scale.h"
 #include "shumate/shumate-map-source.h"
 #include "shumate/shumate-map-source-registry.h"
-#include "shumate/shumate-network-tile-source.h"
+#include "shumate/shumate-tile-downloader.h"
 
 #include "shumate/shumate-memory-cache.h"
 #include "shumate/shumate-file-cache.h"
 
-#include "shumate/shumate-vector-style.h"
+#include "shumate/shumate-vector-renderer.h"
 
 #undef __SHUMATE_SHUMATE_H_INSIDE__
 
diff --git a/shumate/vector/shumate-vector-expression-filter.c 
b/shumate/vector/shumate-vector-expression-filter.c
index ecca473..401278a 100644
--- a/shumate/vector/shumate-vector-expression-filter.c
+++ b/shumate/vector/shumate-vector-expression-filter.c
@@ -16,7 +16,7 @@
  */
 
 
-#include "shumate-vector-style.h"
+#include "shumate-vector-renderer.h"
 #include "shumate-vector-expression-filter-private.h"
 #include "shumate-vector-expression-literal-private.h"
 
diff --git a/shumate/vector/shumate-vector-expression-interpolate.c 
b/shumate/vector/shumate-vector-expression-interpolate.c
index 6597e0e..6bef8bf 100644
--- a/shumate/vector/shumate-vector-expression-interpolate.c
+++ b/shumate/vector/shumate-vector-expression-interpolate.c
@@ -15,7 +15,7 @@
  * License along with this library; if not, see <https://www.gnu.org/licenses/>.
  */
 
-#include "shumate-vector-style.h"
+#include "shumate-vector-renderer.h"
 #include "shumate-vector-expression-interpolate-private.h"
 #include "shumate-vector-expression-literal-private.h"
 #include "shumate-vector-utils-private.h"
diff --git a/shumate/vector/shumate-vector-expression.c b/shumate/vector/shumate-vector-expression.c
index 045bfd1..42d04b1 100644
--- a/shumate/vector/shumate-vector-expression.c
+++ b/shumate/vector/shumate-vector-expression.c
@@ -16,7 +16,7 @@
  */
 
 
-#include "shumate-vector-style.h"
+#include "shumate-vector-renderer.h"
 #include "shumate-vector-expression-private.h"
 #include "shumate-vector-expression-filter-private.h"
 #include "shumate-vector-expression-interpolate-private.h"
diff --git a/shumate/vector/shumate-vector-utils-private.h b/shumate/vector/shumate-vector-utils-private.h
index 6d72720..f0b783f 100644
--- a/shumate/vector/shumate-vector-utils-private.h
+++ b/shumate/vector/shumate-vector-utils-private.h
@@ -18,7 +18,7 @@
 #pragma once
 
 #include <json-glib/json-glib.h>
-#include "shumate-vector-style.h"
+#include "shumate-vector-renderer.h"
 
 
 gboolean shumate_vector_json_get_object (JsonNode *node, JsonObject **dest, GError **error);
diff --git a/tests/file-cache.c b/tests/file-cache.c
index 0d4f1d2..db2017e 100644
--- a/tests/file-cache.c
+++ b/tests/file-cache.c
@@ -44,21 +44,18 @@ static void
 test_file_cache_store_retrieve ()
 {
   g_autoptr(ShumateFileCache) cache = shumate_file_cache_new_full (100000000, "test", NULL);
-  g_autoptr(ShumateTile) tile = shumate_tile_new_full (0, 0, 256, 0);
   g_autoptr(GBytes) bytes = g_bytes_new_static (TEST_DATA, sizeof TEST_DATA);
   g_autoptr(GMainLoop) loop = NULL;
 
-  g_object_ref_sink (tile);
-
   /* Store the tile */
   loop = g_main_loop_new (NULL, TRUE);
-  shumate_file_cache_store_tile_async (cache, tile, bytes, TEST_ETAG, NULL, on_tile_stored, loop);
+  shumate_file_cache_store_tile_async (cache, 0, 0, 256, bytes, TEST_ETAG, NULL, on_tile_stored, loop);
   g_main_loop_run (loop);
 
   /* Now retrieve it */
   g_main_loop_unref (loop);
   loop = g_main_loop_new (NULL, TRUE);
-  shumate_file_cache_get_tile_async (cache, tile, NULL, on_tile_retrieved, loop);
+  shumate_file_cache_get_tile_async (cache, 0, 0, 256, NULL, on_tile_retrieved, loop);
   g_main_loop_run (loop);
 }
 
@@ -87,13 +84,10 @@ static void
 test_file_cache_miss ()
 {
   g_autoptr(ShumateFileCache) cache = shumate_file_cache_new_full (100000000, "test", NULL);
-  g_autoptr(ShumateTile) tile = shumate_tile_new_full (0, 0, 256, 0);
   g_autoptr(GMainLoop) loop = NULL;
 
-  g_object_ref_sink (tile);
-
   loop = g_main_loop_new (NULL, TRUE);
-  shumate_file_cache_get_tile_async (cache, tile, NULL, on_no_tile_retrieved, loop);
+  shumate_file_cache_get_tile_async (cache, 0, 0, 256, NULL, on_no_tile_retrieved, loop);
   g_main_loop_run (loop);
 }
 
diff --git a/tests/license.c b/tests/license.c
index 2fde5cf..81a0adf 100644
--- a/tests/license.c
+++ b/tests/license.c
@@ -7,13 +7,13 @@ test_license_map ()
   g_autoptr(ShumateLicense) license = shumate_license_new ();
   g_autoptr(ShumateMap) map1 = shumate_map_new ();
   ShumateViewport *viewport1 = shumate_map_get_viewport (map1);
-  g_autoptr(ShumateNetworkTileSource) source1 = NULL;
+  g_autoptr(ShumateRasterRenderer) source1 = NULL;
   ShumateLayer *layer1 = NULL;
   g_autoptr(ShumateMap) map2 = shumate_map_new ();
   ShumateViewport *viewport2 = shumate_map_get_viewport (map2);
-  g_autoptr(ShumateNetworkTileSource) source2 = NULL;
+  g_autoptr(ShumateRasterRenderer) source2 = NULL;
   ShumateLayer *layer2 = NULL;
-  g_autoptr(ShumateNetworkTileSource) source2b = NULL;
+  g_autoptr(ShumateRasterRenderer) source2b = NULL;
   ShumateLayer *layer2b = NULL;
   GtkWidget *label;
 
@@ -29,12 +29,12 @@ test_license_map ()
   shumate_license_set_map (license, map1);
   g_assert_cmpstr (gtk_label_get_text (GTK_LABEL (label)), ==, "");
 
-  source1 = shumate_network_tile_source_new_full ("test",
-                                                  "test",
-                                                  "Hello, world!",
-                                                  NULL, 0, 0, 256,
-                                                  SHUMATE_MAP_PROJECTION_MERCATOR,
-                                                  NULL);
+  source1 = shumate_raster_renderer_new_full_from_url ("test",
+                                                       "test",
+                                                       "Hello, world!",
+                                                       NULL, 0, 0, 256,
+                                                       SHUMATE_MAP_PROJECTION_MERCATOR,
+                                                       "https://localhost";);
   layer1 = SHUMATE_LAYER (shumate_map_layer_new (SHUMATE_MAP_SOURCE (source1), viewport1));
   shumate_map_add_layer (map1, layer1);
   g_assert_cmpstr (gtk_label_get_text (GTK_LABEL (label)), ==, "Hello, world!");
@@ -45,24 +45,24 @@ test_license_map ()
   shumate_map_remove_layer (map1, layer1);
   g_assert_cmpstr (gtk_label_get_text (GTK_LABEL (label)), ==, "");
 
-  source2 = shumate_network_tile_source_new_full ("test",
-                                                  "test",
-                                                  "Goodbye, world!",
-                                                  NULL, 0, 0, 256,
-                                                  SHUMATE_MAP_PROJECTION_MERCATOR,
-                                                  NULL);
+  source2 = shumate_raster_renderer_new_full_from_url ("test",
+                                                       "test",
+                                                       "Goodbye, world!",
+                                                       NULL, 0, 0, 256,
+                                                       SHUMATE_MAP_PROJECTION_MERCATOR,
+                                                       "https://localhost";);
   layer2 = SHUMATE_LAYER (shumate_map_layer_new (SHUMATE_MAP_SOURCE (source2), viewport2));
   shumate_map_add_layer (map2, layer2);
 
   shumate_license_set_map (license, map2);
   g_assert_cmpstr (gtk_label_get_text (GTK_LABEL (label)), ==, "Goodbye, world!");
 
-  source2b = shumate_network_tile_source_new_full ("test",
-                                                   "test",
-                                                   "source2b || !source2b",
-                                                   NULL, 0, 0, 256,
-                                                   SHUMATE_MAP_PROJECTION_MERCATOR,
-                                                   NULL);
+  source2b = shumate_raster_renderer_new_full_from_url ("test",
+                                                        "test",
+                                                        "source2b || !source2b",
+                                                        NULL, 0, 0, 256,
+                                                        SHUMATE_MAP_PROJECTION_MERCATOR,
+                                                        "https://localhost";);
   layer2b = SHUMATE_LAYER (shumate_map_layer_new (SHUMATE_MAP_SOURCE (source2b), viewport2));
   shumate_map_insert_layer_above (map2, layer2b, NULL);
 
diff --git a/tests/meson.build b/tests/meson.build
index 1ed5838..24a307d 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -14,7 +14,6 @@ tests = [
   'map',
   'marker-layer',
   'memory-cache',
-  'network-tile-source',
   'viewport',
 ]
 
@@ -28,27 +27,12 @@ endif
 
 subdir('data')
 
-test_utils_sources = [
-  'test-tile-server.c',
-]
-testutils_lib = shared_library(
-  'testutils',
-  test_utils_sources,
-  dependencies: libshumate_dep,
-)
-testutils_dep = declare_dependency(
-  link_with: testutils_lib,
-  dependencies: libshumate_deps,
-  sources: test_utils_sources,
-  include_directories: include_directories('../shumate')
-)
-
 foreach test : tests
   executable = executable(
     test,
     test_resources,
     '@0@.c'.format(test),
-    dependencies: [libshumate_dep, testutils_dep],
+    dependencies: [libshumate_dep],
   )
 
   test(test, executable, env: test_env)
diff --git a/tests/vector-style.c b/tests/vector-style.c
index 0739cb3..05869c6 100644
--- a/tests/vector-style.c
+++ b/tests/vector-style.c
@@ -6,12 +6,12 @@ test_vector_style_create (void)
 {
   GError *error = NULL;
   g_autoptr(GBytes) style_json = NULL;
-  g_autoptr(ShumateVectorStyle) style = NULL;
+  g_autoptr(ShumateVectorRenderer) renderer = NULL;
 
   style_json = g_resources_lookup_data ("/org/gnome/shumate/Tests/style.json", G_RESOURCE_LOOKUP_FLAGS_NONE, 
NULL);
   g_assert_no_error (error);
 
-  style = shumate_vector_style_create (g_bytes_get_data (style_json, NULL), &error);
+  renderer = shumate_vector_renderer_new_from_url ("", g_bytes_get_data (style_json, NULL), &error);
   g_assert_no_error (error);
 }
 


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