L.TileLayer.addInitHook(function() { if (!this.options.useCache) { this._db = null; this._canvas = null; return; } this._db = new PouchDB('offline-tiles'); this._canvas = document.createElement('canvas'); if (!(this._canvas.getContext && this._canvas.getContext('2d'))) { // HTML5 canvas is needed to pack the tiles as base64 data. If // the browser doesn't support canvas, the code will forcefully // skip caching the tiles. this._canvas = null; } }); L.TileLayer.prototype.options.useCache = false; L.TileLayer.prototype.options.saveToCache = true; L.TileLayer.prototype.options.useOnlyCache = false; L.TileLayer.prototype.options.cacheMaxAge = 24*3600*1000; L.TileLayer.include({ // Overwrites L.TileLayer.prototype._loadTile _loadTile: function(tile, tilePoint) { tile._layer = this; tile.onerror = this._tileOnError; this._adjustTilePoint(tilePoint); var tileUrl = this.getTileUrl(tilePoint); this.fire('tileloadstart', { tile: tile, url: tileUrl }); if (this.options.useCache && this._canvas) { this._db.get(tileUrl, {revs_info: true}, this._onCacheLookup(tile,tileUrl)); } else { // Fall back to standard behaviour tile.onload = this._tileOnLoad; tile.src = tileUrl; } }, // Returns a callback (closure over tile/key/originalSrc) to be run when the DB // backend is finished with a fetch operation. _onCacheLookup: function(tile,tileUrl) { return function(err,data) { if (data) { this.fire('tilecachehit', { tile: tile, url: tileUrl }); if (Date.now() > data.timestamp + this.options.cacheMaxAge && !this.options.useOnlyCache) { // Tile is too old, try to refresh it // console.log('Tile is too old: ', tileUrl); if (this.options.saveToCache) { tile.onload = this._saveTile(tileUrl, data._revs_info[0].rev); } tile.crossOrigin = 'Anonymous'; tile.src = tileUrl; tile.onerror = function(ev) { // If the tile is too old but couldn't be fetched from the network, // serve the one still in cache. this.src = data.dataUrl; } } else { // Serve tile from cached data // console.log('Tile is cached: ', tileUrl); tile.onload = this._tileOnLoad; tile.src = data.dataUrl; // data.dataUrl is already a base64-encoded PNG image. } } else { this.fire('tilecachemiss', { tile: tile, url: tileUrl }); if (this.options.useOnlyCache) { // Offline, not cached // console.log('Tile not in cache', tileUrl); tile.onload = this._tileOnLoad; tile.src = L.Util.emptyImageUrl; } else { // Online, not cached, request the tile normally // console.log('Requesting tile normally', tileUrl); if (this.options.saveToCache) { tile.onload = this._saveTile(tileUrl); } else { tile.onload = this._tileOnLoad; } tile.crossOrigin = 'Anonymous'; tile.src = tileUrl; } } }.bind(this); }, // Returns an event handler (closure over DB key), which runs // when the tile (which is an ) is ready. // The handler will delete the document from pouchDB if an existing revision is passed. // This will keep just the latest valid copy of the image in the cache. _saveTile: function(tileUrl, existingRevision) { return function(ev) { if (this._canvas === null) return; var img = ev.target; L.TileLayer.prototype._tileOnLoad.call(img,ev); this._canvas.width = img.naturalWidth || img.width; this._canvas.height = img.naturalHeight || img.height; var context = this._canvas.getContext('2d'); context.drawImage(img, 0, 0); var dataUrl = this._canvas.toDataURL('image/png'); var doc = {dataUrl: dataUrl, timestamp: Date.now()}; if (existingRevision) { this._db.remove(tileUrl, existingRevision); } this._db.put(doc, tileUrl, doc.timestamp); }.bind(this); }, // Seeds the cache given a bounding box (latLngBounds), and // the minimum and maximum zoom levels // Use with care! This can spawn thousands of requests and // flood tileservers! seed: function(bbox, minZoom, maxZoom) { if (minZoom > maxZoom) return; if (!this._map) return; var queue = []; for (var z = minZoom; z