champlain-file-cache.c 26.9 KB
Newer Older
1 2
/*
 * Copyright (C) 2009 Pierre-Luc Beaudoin <pierre-luc@pierlux.com>
Jiří Techet's avatar
Jiří Techet committed
3
 * Copyright (C) 2010-2013 Jiri Techet <techet@gmail.com>
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
 *
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

20 21 22 23
/**
 * SECTION:champlain-file-cache
 * @short_description: Stores and loads cached tiles from the file system
 *
Jiří Techet's avatar
Jiří Techet committed
24
 * #ChamplainFileCache is a cache that stores and retrieves tiles from the
25
 * file system. Tiles most frequently loaded gain in "popularity". This popularity
26 27 28
 * is taken into account when purging the cache.
 */

29 30 31 32 33 34 35 36 37 38 39 40 41 42
#define DEBUG_FLAG CHAMPLAIN_DEBUG_CACHE
#include "champlain-debug.h"

#include "champlain-file-cache.h"

#include <sqlite3.h>
#include <errno.h>
#include <glib.h>
#include <gio/gio.h>
#include <string.h>
#include <stdlib.h>

G_DEFINE_TYPE (ChamplainFileCache, champlain_file_cache, CHAMPLAIN_TYPE_TILE_CACHE);

43
#define GET_PRIVATE(obj) \
44
  (G_TYPE_INSTANCE_GET_PRIVATE ((obj), CHAMPLAIN_TYPE_FILE_CACHE, ChamplainFileCachePrivate))
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62

enum
{
  PROP_0,
  PROP_SIZE_LIMIT,
  PROP_CACHE_DIR
};

struct _ChamplainFileCachePrivate
{
  guint size_limit;
  gchar *cache_dir;

  sqlite3 *db;
  sqlite3_stmt *stmt_select;
  sqlite3_stmt *stmt_update;
};

Jiří Techet's avatar
Jiří Techet committed
63
static void finalize_sql (ChamplainFileCache *file_cache);
64
static void init_cache (ChamplainFileCache *file_cache);
Jiří Techet's avatar
Jiří Techet committed
65 66 67 68 69 70
static gchar *get_filename (ChamplainFileCache *file_cache,
    ChamplainTile *tile);
static gboolean tile_is_expired (ChamplainFileCache *file_cache,
    ChamplainTile *tile);
static void delete_tile (ChamplainFileCache *file_cache,
    const gchar *filename);
71
static gboolean create_cache_dir (const gchar *dir_name);
72 73

static void fill_tile (ChamplainMapSource *map_source,
Jiří Techet's avatar
Jiří Techet committed
74
    ChamplainTile *tile);
75 76

static void store_tile (ChamplainTileCache *tile_cache,
Jiří Techet's avatar
Jiří Techet committed
77 78 79 80 81 82 83
    ChamplainTile *tile,
    const gchar *contents,
    gsize size);
static void refresh_tile_time (ChamplainTileCache *tile_cache,
    ChamplainTile *tile);
static void on_tile_filled (ChamplainTileCache *tile_cache,
    ChamplainTile *tile);
84 85 86

static void
champlain_file_cache_get_property (GObject *object,
Jiří Techet's avatar
Jiří Techet committed
87 88 89
    guint property_id,
    GValue *value,
    GParamSpec *pspec)
90 91 92 93 94 95 96 97
{
  ChamplainFileCache *file_cache = CHAMPLAIN_FILE_CACHE (object);

  switch (property_id)
    {
    case PROP_SIZE_LIMIT:
      g_value_set_uint (value, champlain_file_cache_get_size_limit (file_cache));
      break;
98

99 100 101
    case PROP_CACHE_DIR:
      g_value_set_string (value, champlain_file_cache_get_cache_dir (file_cache));
      break;
102

103 104 105 106 107
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    }
}

108

109 110
static void
champlain_file_cache_set_property (GObject *object,
Jiří Techet's avatar
Jiří Techet committed
111 112 113
    guint property_id,
    const GValue *value,
    GParamSpec *pspec)
114 115
{
  ChamplainFileCache *file_cache = CHAMPLAIN_FILE_CACHE (object);
116
  ChamplainFileCachePrivate *priv = file_cache->priv;
117 118 119 120 121 122

  switch (property_id)
    {
    case PROP_SIZE_LIMIT:
      champlain_file_cache_set_size_limit (file_cache, g_value_get_uint (value));
      break;
123

124
    case PROP_CACHE_DIR:
Jiří Techet's avatar
Jiří Techet committed
125 126
      if (priv->cache_dir)
        g_free (priv->cache_dir);
127 128
      priv->cache_dir = g_strdup (g_value_get_string (value));
      break;
129

130 131 132 133 134
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    }
}

135

136 137 138 139 140 141
static void
champlain_file_cache_dispose (GObject *object)
{
  G_OBJECT_CLASS (champlain_file_cache_parent_class)->dispose (object);
}

142

143
static void
Jiří Techet's avatar
Jiří Techet committed
144
finalize_sql (ChamplainFileCache *file_cache)
145
{
146
  ChamplainFileCachePrivate *priv = file_cache->priv;
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
  gint error;

  if (priv->stmt_select)
    {
      sqlite3_finalize (priv->stmt_select);
      priv->stmt_select = NULL;
    }

  if (priv->stmt_update)
    {
      sqlite3_finalize (priv->stmt_update);
      priv->stmt_update = NULL;
    }

  if (priv->db)
    {
      error = sqlite3_close (priv->db);
      if (error != SQLITE_OK)
        DEBUG ("Sqlite returned error %d when closing cache.db", error);
      priv->db = NULL;
    }
}


static void
champlain_file_cache_finalize (GObject *object)
{
174
  ChamplainFileCache *file_cache = CHAMPLAIN_FILE_CACHE (object);
175
  ChamplainFileCachePrivate *priv = file_cache->priv;
176 177 178

  finalize_sql (file_cache);

Jiří Techet's avatar
Jiří Techet committed
179
  g_free (priv->cache_dir);
180 181 182 183

  G_OBJECT_CLASS (champlain_file_cache_parent_class)->finalize (object);
}

184

185 186 187 188 189 190 191 192 193
static gboolean
create_cache_dir (const gchar *dir_name)
{
  /* If needed, create the cache's dirs */
  if (dir_name)
    {
      if (g_mkdir_with_parents (dir_name, 0700) == -1 && errno != EEXIST)
        {
          g_warning ("Unable to create the image cache path '%s': %s",
194
              dir_name, g_strerror (errno));
195 196 197 198 199 200
          return FALSE;
        }
    }
  return TRUE;
}

201

202
static void
203
init_cache (ChamplainFileCache *file_cache)
204
{
205
  ChamplainFileCachePrivate *priv = file_cache->priv;
206 207 208 209
  gchar *filename = NULL;
  gchar *error_msg = NULL;
  gint error;

210
  g_return_if_fail (create_cache_dir (priv->cache_dir));
211

212
  filename = g_build_filename (priv->cache_dir,
Jiří Techet's avatar
Jiří Techet committed
213
        "cache.db", NULL);
214
  error = sqlite3_open_v2 (filename, &priv->db,
Jiří Techet's avatar
Jiří Techet committed
215
        SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL);
216 217 218 219 220 221 222 223 224
  g_free (filename);

  if (error == SQLITE_ERROR)
    {
      DEBUG ("Sqlite returned error %d when opening cache.db", error);
      return;
    }

  sqlite3_exec (priv->db,
Jiří Techet's avatar
Jiří Techet committed
225 226 227
      "PRAGMA synchronous=OFF;"
      "PRAGMA count_changes=OFF;",
      NULL, NULL, &error_msg);
228 229 230 231 232 233 234 235
  if (error_msg != NULL)
    {
      DEBUG ("Set PRAGMA: %s", error_msg);
      sqlite3_free (error_msg);
      return;
    }

  sqlite3_exec (priv->db,
Jiří Techet's avatar
Jiří Techet committed
236 237 238 239 240 241
      "CREATE TABLE IF NOT EXISTS tiles ("
      "filename TEXT PRIMARY KEY, "
      "etag TEXT, "
      "popularity INT DEFAULT 1, "
      "size INT DEFAULT 0)",
      NULL, NULL, &error_msg);
242 243 244 245 246 247 248 249
  if (error_msg != NULL)
    {
      DEBUG ("Creating table 'tiles' failed: %s", error_msg);
      sqlite3_free (error_msg);
      return;
    }

  error = sqlite3_prepare_v2 (priv->db,
Jiří Techet's avatar
Jiří Techet committed
250 251
        "SELECT etag FROM tiles WHERE filename = ?", -1,
        &priv->stmt_select, NULL);
252 253 254 255
  if (error != SQLITE_OK)
    {
      priv->stmt_select = NULL;
      DEBUG ("Failed to prepare the select Etag statement, error:%d: %s",
Jiří Techet's avatar
Jiří Techet committed
256
          error, sqlite3_errmsg (priv->db));
257 258 259 260
      return;
    }

  error = sqlite3_prepare_v2 (priv->db,
Jiří Techet's avatar
Jiří Techet committed
261 262
        "UPDATE tiles SET popularity = popularity + 1 WHERE filename = ?", -1,
        &priv->stmt_update, NULL);
263 264 265 266
  if (error != SQLITE_OK)
    {
      priv->stmt_update = NULL;
      DEBUG ("Failed to prepare the update popularity statement, error: %s",
Jiří Techet's avatar
Jiří Techet committed
267
          sqlite3_errmsg (priv->db));
268 269 270 271 272 273
      return;
    }

  g_object_notify (G_OBJECT (file_cache), "cache-dir");
}

274

Jiří Techet's avatar
Jiří Techet committed
275 276 277
static void
champlain_file_cache_constructed (GObject *object)
{
278
  ChamplainFileCache *file_cache = CHAMPLAIN_FILE_CACHE (object);
279
  ChamplainFileCachePrivate *priv = file_cache->priv;
280 281 282

  if (!priv->cache_dir)
    {
283
#ifdef CHAMPLAIN_HAS_MAEMO
284
      priv->cache_dir = g_strdup ("/home/user/MyDocs/.Maps/");
285
#else
286
      priv->cache_dir = g_build_path (G_DIR_SEPARATOR_S,
Jiří Techet's avatar
Jiří Techet committed
287 288
            g_get_user_cache_dir (),
            "champlain", NULL);
289 290
#endif
    }
Jiří Techet's avatar
Jiří Techet committed
291 292 293 294 295 296

  init_cache (file_cache);

  G_OBJECT_CLASS (champlain_file_cache_parent_class)->constructed (object);
}

297

298 299 300 301 302 303 304
static void
champlain_file_cache_class_init (ChamplainFileCacheClass *klass)
{
  ChamplainMapSourceClass *map_source_class = CHAMPLAIN_MAP_SOURCE_CLASS (klass);
  ChamplainTileCacheClass *tile_cache_class = CHAMPLAIN_TILE_CACHE_CLASS (klass);
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GParamSpec *pspec;
305
  gchar *cache_dir = NULL;
306 307 308 309 310 311 312

  g_type_class_add_private (klass, sizeof (ChamplainFileCachePrivate));

  object_class->finalize = champlain_file_cache_finalize;
  object_class->dispose = champlain_file_cache_dispose;
  object_class->get_property = champlain_file_cache_get_property;
  object_class->set_property = champlain_file_cache_set_property;
Jiří Techet's avatar
Jiří Techet committed
313
  object_class->constructed = champlain_file_cache_constructed;
314

315
  /**
316 317 318 319
   * ChamplainFileCache:size-limit:
   *
   * The cache size limit in bytes.
   *
320
   * Note: this new value will not be applied until you call champlain_file_cache_purge()
321 322 323
   *
   * Since: 0.4
   */
324
  pspec = g_param_spec_uint ("size-limit",
Jiří Techet's avatar
Jiří Techet committed
325 326 327 328 329 330
        "Size Limit",
        "The cache's size limit (Mb)",
        1,
        G_MAXINT,
        100000000,
        G_PARAM_CONSTRUCT | G_PARAM_READWRITE);
331 332
  g_object_class_install_property (object_class, PROP_SIZE_LIMIT, pspec);

333
  /**
334 335 336 337 338 339
   * ChamplainFileCache:cache-dir:
   *
   * The directory where the tile database is stored.
   *
   * Since: 0.6
   */
340
  pspec = g_param_spec_string ("cache-dir",
Jiří Techet's avatar
Jiří Techet committed
341 342 343 344
        "Cache Directory",
        "The directory of the cache",
        cache_dir,
        G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE);
345 346 347 348 349 350 351 352 353
  g_object_class_install_property (object_class, PROP_CACHE_DIR, pspec);

  tile_cache_class->store_tile = store_tile;
  tile_cache_class->refresh_tile_time = refresh_tile_time;
  tile_cache_class->on_tile_filled = on_tile_filled;

  map_source_class->fill_tile = fill_tile;
}

354

355 356 357 358 359
static void
champlain_file_cache_init (ChamplainFileCache *file_cache)
{
  ChamplainFileCachePrivate *priv = GET_PRIVATE (file_cache);

360 361
  file_cache->priv = priv;

362
  priv->cache_dir = NULL;
363
  priv->size_limit = 100000000;
364
  priv->cache_dir = NULL;
365 366 367 368 369
  priv->db = NULL;
  priv->stmt_select = NULL;
  priv->stmt_update = NULL;
}

370

371 372
/**
 * champlain_file_cache_new_full:
373
 * @size_limit: maximum size of the cache in bytes
374
 * @cache_dir: (allow-none): the directory where the cache is created. When cache_dir == NULL,
375
 * a cache in ~/.cache/champlain is used.
Jiří Techet's avatar
Jiří Techet committed
376
 * @renderer: the #ChamplainRenderer used for tiles rendering
377 378 379 380 381
 *
 * Constructor of #ChamplainFileCache.
 *
 * Returns: a constructed #ChamplainFileCache
 *
Jiří Techet's avatar
Jiří Techet committed
382
 * Since: 0.8
383
 */
384 385
ChamplainFileCache *
champlain_file_cache_new_full (guint size_limit,
386 387
    const gchar *cache_dir,
    ChamplainRenderer *renderer)
388
{
389 390 391
  ChamplainFileCache *cache;

  cache = g_object_new (CHAMPLAIN_TYPE_FILE_CACHE,
Jiří Techet's avatar
Jiří Techet committed
392 393 394 395
        "size-limit", size_limit,
        "cache-dir", cache_dir,
        "renderer", renderer,
        NULL);
396 397 398
  return cache;
}

399

400 401
/**
 * champlain_file_cache_get_size_limit:
402
 * @file_cache: a #ChamplainFileCache
403 404 405 406 407 408 409
 *
 * Gets the cache size limit in bytes.
 *
 * Returns: size limit
 *
 * Since: 0.4
 */
410 411 412
guint
champlain_file_cache_get_size_limit (ChamplainFileCache *file_cache)
{
Jiří Techet's avatar
Jiří Techet committed
413
  g_return_val_if_fail (CHAMPLAIN_IS_FILE_CACHE (file_cache), 0);
414

415
  return file_cache->priv->size_limit;
416 417
}

418

419 420
/**
 * champlain_file_cache_get_cache_dir:
421
 * @file_cache: a #ChamplainFileCache
422 423 424 425 426 427 428
 *
 * Gets the directory where the cache database is stored.
 *
 * Returns: the directory
 *
 * Since: 0.6
 */
429 430 431 432 433
const gchar *
champlain_file_cache_get_cache_dir (ChamplainFileCache *file_cache)
{
  g_return_val_if_fail (CHAMPLAIN_IS_FILE_CACHE (file_cache), NULL);

434
  return file_cache->priv->cache_dir;
435 436
}

437

438 439
/**
 * champlain_file_cache_set_size_limit:
440
 * @file_cache: a #ChamplainFileCache
441 442 443 444 445 446
 * @size_limit: the cache limit in bytes
 *
 * Sets the cache size limit in bytes.
 *
 * Since: 0.4
 */
447 448
void
champlain_file_cache_set_size_limit (ChamplainFileCache *file_cache,
Jiří Techet's avatar
Jiří Techet committed
449
    guint size_limit)
450 451 452
{
  g_return_if_fail (CHAMPLAIN_IS_FILE_CACHE (file_cache));

453
  ChamplainFileCachePrivate *priv = file_cache->priv;
454 455 456 457 458

  priv->size_limit = size_limit;
  g_object_notify (G_OBJECT (file_cache), "size-limit");
}

459

460
static gchar *
Jiří Techet's avatar
Jiří Techet committed
461 462
get_filename (ChamplainFileCache *file_cache,
    ChamplainTile *tile)
463
{
464
  ChamplainFileCachePrivate *priv = file_cache->priv;
465 466

  g_return_val_if_fail (CHAMPLAIN_IS_FILE_CACHE (file_cache), NULL);
Jiří Techet's avatar
Jiří Techet committed
467
  g_return_val_if_fail (CHAMPLAIN_IS_TILE (tile), NULL);
468
  g_return_val_if_fail (priv->cache_dir, NULL);
469

470
  ChamplainMapSource *map_source = CHAMPLAIN_MAP_SOURCE (file_cache);
471 472

  gchar *filename = g_strdup_printf ("%s" G_DIR_SEPARATOR_S
Jiří Techet's avatar
Jiří Techet committed
473 474 475 476 477 478 479 480
        "%s" G_DIR_SEPARATOR_S
        "%d" G_DIR_SEPARATOR_S
        "%d" G_DIR_SEPARATOR_S "%d.png",
        priv->cache_dir,
        champlain_map_source_get_id (map_source),
        champlain_tile_get_zoom_level (tile),
        champlain_tile_get_x (tile),
        champlain_tile_get_y (tile));
481 482 483
  return filename;
}

484

485
static gboolean
Jiří Techet's avatar
Jiří Techet committed
486 487
tile_is_expired (ChamplainFileCache *file_cache,
    ChamplainTile *tile)
488
{
Jiří Techet's avatar
Jiří Techet committed
489 490
  g_return_val_if_fail (CHAMPLAIN_FILE_CACHE (file_cache), FALSE);
  g_return_val_if_fail (CHAMPLAIN_TILE (tile), FALSE);
491

492
  GTimeVal now = { 0, };
493 494 495 496 497 498
  const GTimeVal *modified_time = champlain_tile_get_modified_time (tile);
  gboolean validate_cache = TRUE;

  if (modified_time)
    {
      g_get_current_time (&now);
499
      g_time_val_add (&now, (-24ul * 60ul * 60ul * 1000ul * 1000ul * 7ul)); /* Cache expires in 7 days */
500 501 502
      validate_cache = modified_time->tv_sec < now.tv_sec;
    }

503
  DEBUG ("%p is %s expired", tile, (validate_cache ? "" : "not"));
504 505 506 507

  return validate_cache;
}

508

509
typedef struct
510
{
511 512
  ChamplainMapSource *map_source;
  ChamplainTile *tile;
513
} FileLoadedData;
514

515
static void
516
tile_rendered_cb (ChamplainTile *tile,
517 518 519
    gpointer data,
    guint size,
    gboolean error,
520
    FileLoadedData *user_data)
521 522
{
  ChamplainMapSource *map_source = user_data->map_source;
523
  GFile *file;
524 525 526
  ChamplainFileCache *file_cache;
  ChamplainMapSource *next_source;
  ChamplainFileCachePrivate *priv;
527
  GFileInfo *info = NULL;
528
  GTimeVal modified_time = { 0, };
529
  gchar *filename = NULL;
530

531
  g_signal_handlers_disconnect_by_func (tile, tile_rendered_cb, user_data);
532
  g_slice_free (FileLoadedData, user_data);
533

534 535 536 537
  next_source = champlain_map_source_get_next_source (map_source);
  file_cache = CHAMPLAIN_FILE_CACHE (map_source);
  priv = file_cache->priv;

538
  if (error)
539
    {
540
      DEBUG ("Tile rendering failed");
541 542 543
      goto load_next;
    }

544
  champlain_tile_set_state (tile, CHAMPLAIN_STATE_LOADED);
545

546
  filename = get_filename (file_cache, tile);
547
  file = g_file_new_for_path (filename);
548 549

  /* Retrieve modification time */
550
  info = g_file_query_info (file,
Jiří Techet's avatar
Jiří Techet committed
551 552
        G_FILE_ATTRIBUTE_TIME_MODIFIED,
        G_FILE_QUERY_INFO_NONE, NULL, NULL);
553 554 555 556
  if (info)
    {
      g_file_info_get_modification_time (info, &modified_time);
      champlain_tile_set_modified_time (tile, &modified_time);
557 558

      g_object_unref (info);
559 560 561 562
    }
  g_object_unref (file);

  /* Notify other caches that the tile has been filled */
563
  if (CHAMPLAIN_IS_TILE_CACHE (next_source))
Jiří Techet's avatar
Jiří Techet committed
564
    champlain_tile_cache_on_tile_filled (CHAMPLAIN_TILE_CACHE (next_source), tile);
565 566 567 568 569 570

  if (tile_is_expired (file_cache, tile))
    {
      int sql_rc = SQLITE_OK;

      /* Retrieve etag */
571
      sqlite3_reset (priv->stmt_select);
572 573 574 575
      sql_rc = sqlite3_bind_text (priv->stmt_select, 1, filename, -1, SQLITE_STATIC);
      if (sql_rc == SQLITE_ERROR)
        {
          DEBUG ("Failed to prepare the SQL query for finding the Etag of '%s', error: %s",
Jiří Techet's avatar
Jiří Techet committed
576
              filename, sqlite3_errmsg (priv->db));
577 578 579 580 581 582 583 584 585 586 587 588
          goto load_next;
        }

      sql_rc = sqlite3_step (priv->stmt_select);
      if (sql_rc == SQLITE_ROW)
        {
          const gchar *etag = (const gchar *) sqlite3_column_text (priv->stmt_select, 0);
          champlain_tile_set_etag (CHAMPLAIN_TILE (tile), etag);
        }
      else if (sql_rc == SQLITE_DONE)
        {
          DEBUG ("'%s' does't have an etag",
Jiří Techet's avatar
Jiří Techet committed
589
              filename);
590 591 592 593 594
          goto load_next;
        }
      else if (sql_rc == SQLITE_ERROR)
        {
          DEBUG ("Failed to finding the Etag of '%s', %d error: %s",
Jiří Techet's avatar
Jiří Techet committed
595
              filename, sql_rc, sqlite3_errmsg (priv->db));
596 597 598 599 600 601 602 603 604
          goto load_next;
        }

      /* Validate the tile */
      /* goto load_next; */
    }
  else
    {
      /* Tile loaded and no validation needed - done */
605
      champlain_tile_set_fade_in (tile, FALSE);
606
      champlain_tile_set_state (tile, CHAMPLAIN_STATE_DONE);
607
      champlain_tile_display_content (tile);
608 609 610 611
      goto cleanup;
    }

load_next:
612
  if (CHAMPLAIN_IS_MAP_SOURCE (next_source))
Jiří Techet's avatar
Jiří Techet committed
613
    champlain_map_source_fill_tile (next_source, tile);
614
  else if (champlain_tile_get_state (tile) == CHAMPLAIN_STATE_LOADED)
615 616 617 618 619
    {
      /* if we have some content, use the tile even if it wasn't validated */
      champlain_tile_set_state (tile, CHAMPLAIN_STATE_DONE);
      champlain_tile_display_content (tile);
    }
620 621

cleanup:
Jiří Techet's avatar
Jiří Techet committed
622
  g_free (filename);
623 624
  g_object_unref (tile);
  g_object_unref (map_source);
625 626 627
}


628
static void
629 630
file_loaded_cb (GFile *file,
    GAsyncResult *res,
631
    FileLoadedData *user_data)
632 633 634 635 636 637 638 639
{
  gboolean ok;
  gchar *contents;
  gsize length;
  GError *error = NULL;
  ChamplainTile *tile = user_data->tile;
  ChamplainMapSource *map_source = user_data->map_source;
  ChamplainRenderer *renderer;
640

641 642 643 644
  ok = g_file_load_contents_finish (file, res, &contents, &length, NULL, &error);

  if (!ok)
    {
645 646
      gchar *path;

Jiří Techet's avatar
Jiří Techet committed
647
      path = g_file_get_path (file);
648 649
      DEBUG ("Failed to load tile %s, error: %s", path, error->message);
      g_free (path);
650 651 652 653
      contents = NULL;
      length = 0;
      g_error_free (error);
    }
654

655 656 657 658 659 660
  g_object_unref (file);

  renderer = champlain_map_source_get_renderer (map_source);

  g_return_if_fail (CHAMPLAIN_IS_RENDERER (renderer));

661
  g_signal_connect (tile, "render-complete", G_CALLBACK (tile_rendered_cb), user_data);
662

663
  champlain_renderer_set_data (renderer, contents, length);
Jiří Techet's avatar
Jiří Techet committed
664
  g_free (contents);
665
  champlain_renderer_render (renderer, tile);
666 667
}

668

669 670
static void
fill_tile (ChamplainMapSource *map_source,
Jiří Techet's avatar
Jiří Techet committed
671
    ChamplainTile *tile)
672 673 674 675
{
  g_return_if_fail (CHAMPLAIN_IS_FILE_CACHE (map_source));
  g_return_if_fail (CHAMPLAIN_IS_TILE (tile));

676 677
  ChamplainMapSource *next_source = champlain_map_source_get_next_source (map_source);

678 679 680
  if (champlain_tile_get_state (tile) == CHAMPLAIN_STATE_DONE)
    return;

681
  if (champlain_tile_get_state (tile) != CHAMPLAIN_STATE_LOADED)
682
    {
683 684 685
      FileLoadedData *user_data;
      gchar *filename;
      GFile *file;
686

687 688
      filename = get_filename (CHAMPLAIN_FILE_CACHE (map_source), tile);
      file = g_file_new_for_path (filename);
689
      g_free (filename);
690 691 692 693

      user_data = g_slice_new (FileLoadedData);
      user_data->tile = tile;
      user_data->map_source = map_source;
694

695 696
      g_object_ref (tile);
      g_object_ref (map_source);
697 698 699 700

      DEBUG ("fill of %s", filename);

      g_file_load_contents_async (file, NULL, (GAsyncReadyCallback) file_loaded_cb, user_data);
701
    }
702 703 704
  else if (CHAMPLAIN_IS_MAP_SOURCE (next_source))
    champlain_map_source_fill_tile (next_source, tile);
  else if (champlain_tile_get_state (tile) == CHAMPLAIN_STATE_LOADED)
705
    {
706 707 708
      /* if we have some content, use the tile even if it wasn't validated */
      champlain_tile_set_state (tile, CHAMPLAIN_STATE_DONE);
      champlain_tile_display_content (tile);
709
    }
710 711
}

712

713
static void
Jiří Techet's avatar
Jiří Techet committed
714 715
refresh_tile_time (ChamplainTileCache *tile_cache,
    ChamplainTile *tile)
716 717 718
{
  g_return_if_fail (CHAMPLAIN_IS_FILE_CACHE (tile_cache));

719
  ChamplainMapSource *map_source = CHAMPLAIN_MAP_SOURCE (tile_cache);
Jiří Techet's avatar
Jiří Techet committed
720
  ChamplainMapSource *next_source = champlain_map_source_get_next_source (map_source);
721
  ChamplainFileCache *file_cache = CHAMPLAIN_FILE_CACHE (tile_cache);
722 723 724 725 726 727 728 729 730
  gchar *filename = NULL;
  GFile *file;
  GFileInfo *info;

  filename = get_filename (file_cache, tile);
  file = g_file_new_for_path (filename);
  g_free (filename);

  info = g_file_query_info (file, G_FILE_ATTRIBUTE_TIME_MODIFIED,
Jiří Techet's avatar
Jiří Techet committed
731
        G_FILE_QUERY_INFO_NONE, NULL, NULL);
732 733 734

  if (info)
    {
735
      GTimeVal now = { 0, };
736 737 738 739 740

      g_get_current_time (&now);

      g_file_info_set_modification_time (info, &now);
      g_file_set_attributes_from_info (file, info, G_FILE_QUERY_INFO_NONE, NULL, NULL);
741

742
      g_object_unref (info);
Jiří Techet's avatar
Jiří Techet committed
743
    }
744 745

  g_object_unref (file);
746

747 748
  if (CHAMPLAIN_IS_TILE_CACHE (next_source))
    champlain_tile_cache_refresh_tile_time (CHAMPLAIN_TILE_CACHE (next_source), tile);
749 750
}

751

752 753
static void
store_tile (ChamplainTileCache *tile_cache,
Jiří Techet's avatar
Jiří Techet committed
754 755 756
    ChamplainTile *tile,
    const gchar *contents,
    gsize size)
757 758 759
{
  g_return_if_fail (CHAMPLAIN_IS_FILE_CACHE (tile_cache));

760
  ChamplainMapSource *map_source = CHAMPLAIN_MAP_SOURCE (tile_cache);
Jiří Techet's avatar
Jiří Techet committed
761
  ChamplainMapSource *next_source = champlain_map_source_get_next_source (map_source);
762
  ChamplainFileCache *file_cache = CHAMPLAIN_FILE_CACHE (tile_cache);
763
  ChamplainFileCachePrivate *priv = file_cache->priv;
764 765 766 767 768 769 770 771 772 773 774 775
  gchar *query = NULL;
  gchar *error = NULL;
  gchar *path = NULL;
  gchar *filename = NULL;
  GError *gerror = NULL;
  GFile *file;
  GFileOutputStream *ostream;
  gsize bytes_written;

  DEBUG ("Update of %p", tile);

  filename = get_filename (file_cache, tile);
Jiří Techet's avatar
Jiří Techet committed
776
  file = g_file_new_for_path (filename);
777 778

  /* If the file exists, delete it */
Jiří Techet's avatar
Jiří Techet committed
779
  g_file_delete (file, NULL, NULL);
780 781 782 783 784 785 786 787

  /* If needed, create the cache's dirs */
  path = g_path_get_dirname (filename);
  if (g_mkdir_with_parents (path, 0700) == -1)
    {
      if (errno != EEXIST)
        {
          g_warning ("Unable to create the image cache path '%s': %s",
788
              path, g_strerror (errno));
789 790 791 792
          goto store_next;
        }
    }

Jiří Techet's avatar
Jiří Techet committed
793
  ostream = g_file_create (file, G_FILE_CREATE_PRIVATE, NULL, &gerror);
794 795 796 797 798 799 800 801
  if (!ostream)
    {
      DEBUG ("GFileOutputStream creation failed: %s", gerror->message);
      g_error_free (gerror);
      goto store_next;
    }

  /* Write the cache */
802
  if (!g_output_stream_write_all (G_OUTPUT_STREAM (ostream), contents, size, &bytes_written, NULL, &gerror))
803 804 805
    {
      DEBUG ("Writing file contents failed: %s", gerror->message);
      g_error_free (gerror);
Jiří Techet's avatar
Jiří Techet committed
806
      g_object_unref (ostream);
807 808 809
      goto store_next;
    }

Jiří Techet's avatar
Jiří Techet committed
810
  g_object_unref (ostream);
811 812

  query = sqlite3_mprintf ("REPLACE INTO tiles (filename, etag, size) VALUES (%Q, %Q, %d)",
Jiří Techet's avatar
Jiří Techet committed
813 814 815
        filename,
        champlain_tile_get_etag (tile),
        size);
816 817 818 819 820 821 822 823 824
  sqlite3_exec (priv->db, query, NULL, NULL, &error);
  if (error != NULL)
    {
      DEBUG ("Saving Etag and size failed: %s", error);
      sqlite3_free (error);
    }
  sqlite3_free (query);

store_next:
825 826
  if (CHAMPLAIN_IS_TILE_CACHE (next_source))
    champlain_tile_cache_store_tile (CHAMPLAIN_TILE_CACHE (next_source), tile, contents, size);
827

Jiří Techet's avatar
Jiří Techet committed
828 829 830
  g_free (filename);
  g_free (path);
  g_object_unref (file);
831 832
}

833

834
static void
Jiří Techet's avatar
Jiří Techet committed
835 836
on_tile_filled (ChamplainTileCache *tile_cache,
    ChamplainTile *tile)
837
{
Jiří Techet's avatar
Jiří Techet committed
838 839
  g_return_if_fail (CHAMPLAIN_IS_FILE_CACHE (tile_cache));
  g_return_if_fail (CHAMPLAIN_IS_TILE (tile));
840

841
  ChamplainMapSource *map_source = CHAMPLAIN_MAP_SOURCE (tile_cache);
Jiří Techet's avatar
Jiří Techet committed
842
  ChamplainMapSource *next_source = champlain_map_source_get_next_source (map_source);
843
  ChamplainFileCache *file_cache = CHAMPLAIN_FILE_CACHE (tile_cache);
844
  ChamplainFileCachePrivate *priv = file_cache->priv;
845 846 847 848 849 850 851
  int sql_rc = SQLITE_OK;
  gchar *filename = NULL;

  filename = get_filename (file_cache, tile);

  DEBUG ("popularity of %s", filename);

852
  sqlite3_reset (priv->stmt_update);
853 854 855 856
  sql_rc = sqlite3_bind_text (priv->stmt_update, 1, filename, -1, SQLITE_STATIC);
  if (sql_rc != SQLITE_OK)
    {
      DEBUG ("Failed to set values to the popularity query of '%s', error: %s",
857
          filename, sqlite3_errmsg (priv->db));
858 859 860 861 862 863 864 865 866 867 868
      goto call_next;
    }

  sql_rc = sqlite3_step (priv->stmt_update);
  if (sql_rc != SQLITE_DONE)
    {
      /* may not be present in this cache */
      goto call_next;
    }

call_next:
869
  g_free (filename);
870 871
  if (CHAMPLAIN_IS_TILE_CACHE (next_source))
    champlain_tile_cache_on_tile_filled (CHAMPLAIN_TILE_CACHE (next_source), tile);
872 873 874 875 876 877 878 879 880 881 882
}


static void
delete_tile (ChamplainFileCache *file_cache, const gchar *filename)
{
  g_return_if_fail (CHAMPLAIN_IS_FILE_CACHE (file_cache));
  gchar *query, *error = NULL;
  GError *gerror = NULL;
  GFile *file;

883
  ChamplainFileCachePrivate *priv = file_cache->priv;
884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902

  query = sqlite3_mprintf ("DELETE FROM tiles WHERE filename = %Q", filename);
  sqlite3_exec (priv->db, query, NULL, NULL, &error);
  if (error != NULL)
    {
      DEBUG ("Deleting tile from db failed: %s", error);
      sqlite3_free (error);
    }
  sqlite3_free (query);

  file = g_file_new_for_path (filename);
  if (!g_file_delete (file, NULL, &gerror))
    {
      DEBUG ("Deleting tile from disk failed: %s", gerror->message);
      g_error_free (gerror);
    }
  g_object_unref (file);
}

903

904 905 906 907 908 909 910
static gboolean
purge_on_idle (gpointer data)
{
  champlain_file_cache_purge (CHAMPLAIN_FILE_CACHE (data));
  return FALSE;
}

911

912 913
/**
 * champlain_file_cache_purge_on_idle:
914
 * @file_cache: a #ChamplainFileCache
915 916 917 918 919 920
 *
 * Purge the cache from the less popular tiles until cache's size limit is reached.
 * This is a non blocking call as the purge will happen when the application is idle
 *
 * Since: 0.4
 */
921 922 923 924
void
champlain_file_cache_purge_on_idle (ChamplainFileCache *file_cache)
{
  g_return_if_fail (CHAMPLAIN_IS_FILE_CACHE (file_cache));
925
  g_idle_add_full (CLUTTER_PRIORITY_REDRAW,
926 927 928
      (GSourceFunc) purge_on_idle,
      g_object_ref (file_cache),
      (GDestroyNotify) g_object_unref);
929 930
}

931

932 933
/**
 * champlain_file_cache_purge:
934
 * @file_cache: a #ChamplainFileCache
935 936 937 938 939
 *
 * Purge the cache from the less popular tiles until cache's size limit is reached.
 *
 * Since: 0.4
 */
940 941 942 943 944
void
champlain_file_cache_purge (ChamplainFileCache *file_cache)
{
  g_return_if_fail (CHAMPLAIN_IS_FILE_CACHE (file_cache));

945
  ChamplainFileCachePrivate *priv = file_cache->priv;
946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963
  gchar *query;
  sqlite3_stmt *stmt;
  int rc = 0;
  guint current_size = 0;
  guint highest_popularity = 0;
  gchar *error;

  query = "SELECT SUM (size) FROM tiles";
  rc = sqlite3_prepare (priv->db, query, strlen (query), &stmt, NULL);
  if (rc != SQLITE_OK)
    {
      DEBUG ("Can't compute cache size %s", sqlite3_errmsg (priv->db));
    }

  rc = sqlite3_step (stmt);
  if (rc != SQLITE_ROW)
    {
      DEBUG ("Failed to count the total cache consumption %s",
964
          sqlite3_errmsg (priv->db));
965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983
      sqlite3_finalize (stmt);
      return;
    }

  current_size = sqlite3_column_int (stmt, 0);
  if (current_size < priv->size_limit)
    {
      DEBUG ("Cache doesn't need to be purged at %d bytes", current_size);
      sqlite3_finalize (stmt);
      return;
    }

  sqlite3_finalize (stmt);

  /* Ok, delete the less popular tiles until size_limit reached */
  query = "SELECT filename, size, popularity FROM tiles ORDER BY popularity";
  rc = sqlite3_prepare (priv->db, query, strlen (query), &stmt, NULL);
  if (rc != SQLITE_OK)
    {
Jiří Techet's avatar
Jiří Techet committed
984
      DEBUG ("Can't fetch tiles to delete: %s", sqlite3_errmsg (priv->db));
985 986 987 988 989
    }

  rc = sqlite3_step (stmt);
  while (rc == SQLITE_ROW && current_size > priv->size_limit)
    {
990
      const char *filename;
991 992
      guint size;

Jiří Techet's avatar
Jiří Techet committed
993
      filename = (const char *) sqlite3_column_text (stmt, 0);
994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008
      size = sqlite3_column_int (stmt, 1);
      highest_popularity = sqlite3_column_int (stmt, 2);
      DEBUG ("Deleting %s of size %d", filename, size);

      delete_tile (file_cache, filename);

      current_size -= size;

      rc = sqlite3_step (stmt);
    }
  DEBUG ("Cache size is now %d", current_size);

  sqlite3_finalize (stmt);

  query = sqlite3_mprintf ("UPDATE tiles SET popularity = popularity - %d",
Jiří Techet's avatar
Jiří Techet committed
1009
        highest_popularity);
1010 1011 1012 1013 1014 1015 1016 1017
  sqlite3_exec (priv->db, query, NULL, NULL, &error);
  if (error != NULL)
    {
      DEBUG ("Updating popularity failed: %s", error);
      sqlite3_free (error);
    }
  sqlite3_free (query);
}