Skip to content
This repository has been archived by the owner on Nov 21, 2024. It is now read-only.

improvement proposal #31

Open
gugurete opened this issue Jun 12, 2017 · 1 comment
Open

improvement proposal #31

gugurete opened this issue Jun 12, 2017 · 1 comment

Comments

@gugurete
Copy link

By using xhr requests instead of canvas rendering you could have a major performance improvement.
I do not have time for a proper pull request, but here is my take on pouchdb caching

import L from 'leaflet'
import PouchDB from '/imports/ui/components/pouchdb'
import qwest from 'qwest'
import logger from '../../logger'

L.TileLayer.addInitHook(function () {
if (this.options.cache) {
this.pouch = new PouchDB(tiles-${this.options.cacheName}, {
size: 1000,
auto_compaction: true,
revs_limit: 1
})
logger.info(pouchdb: ${this.pouch.name}, adapter: ${this.pouch.adapter})
}
})

// Whether to use a PouchDB cache on this tile layer, or not
L.TileLayer.prototype.options.cache = false;

// Maximum age of the cache, in milliseconds
L.TileLayer.prototype.options.cacheMaxAge = 24 * 3600 * 1000;

L.TileLayer.include({

// Overwrites L.TileLayer.prototype.createTile
createTile(coords, done) {
const url = this.getTileUrl(coords);
const tile = document.createElement('img');
if (this.options.crossOrigin) {
tile.crossOrigin = '';
}
tile.onerror = () => {
logger.error(bom.leaf load failed: ${tile.src})
}
if (this.options.cache) {
this.pouch.get(url, {attachments: true, binary: true, revs_info: true}, this.onCacheLookup(tile, url, done));
} else {
// Fall back to standard behaviour
tile.onload = L.bind(this._tileOnLoad, this, done, tile);
tile.src = url;
}
return tile;
},

// Returns a callback (closure over tile/key/originalSrc) to be run when the DB backend is finished with a fetch operation.
onCacheLookup(tile, tileUrl, done) {
return function(err, data) {
if (data) {
this.fire('tilecachehit', {tile, url: tileUrl});
if ((Date.now() > (data.timestamp + this.options.cacheMaxAge))) {
// old tile, refresh it
tile.qwest = null
this.readAndSaveTile(tile, tileUrl, data._revs_info[0].rev, done)
} else {
// tile from cache
tile.onload = L.bind(this._tileOnLoad, this, done, tile);
tile.src = URL.createObjectURL(data._attachments.img.data);
}
} else {
this.fire('tilecachemiss', {tile, url: tileUrl});
this.readAndSaveTile(tile, tileUrl, null, done)
}
}.bind(this);
},

readAndSaveTile(tile, tileUrl, existingRevision, done) {
if (tile.qwest) {
return
}
let headers = {}
if (tileUrl.indexOf('cors-anywhere') >= 0) {
headers = {
'X-Requested-With': 'XMLHttpRequest'
}
}
tile.qwest = qwest.get(tileUrl, null, {headers, cache: true, responseType: 'blob'}).then(res => {
this.saveTile(res.response, tileUrl, existingRevision, done)
tile.src = URL.createObjectURL(res.response);
}).catch(error => {
logger.error('qwest error', error.message);
})
},

// Overwrite L.TileLayer.prototype._abortLoading
_abortLoading() {
let i
let tile
for (i in this._tiles) {
if (this._tiles[i].coords.z !== this._tileZoom) {
tile = this._tiles[i].el;
tile.onload = L.Util.falseFn;
tile.onerror = L.Util.falseFn;
if (!tile.complete) {
tile.src = L.Util.emptyImageUrl;
L.DomUtil.remove(tile);
}
if (tile.qwest) {
tile.qwest.abort()
}
}
}
},

// 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(tileBlob, tileUrl, existingRevision, done) {
const doc = {
_id: tileUrl,
_attachments: {
img: {
content_type: 'image/jpeg',
data: tileBlob
}
},
timestamp: Date.now()
}
if (existingRevision) {
this.pouch.remove(tileUrl, existingRevision, () => {
this.pouchPut(doc)
})
} else {
this.pouchPut(doc)
}
if (done) {
done()
}
},

pouchPut(doc) {
this.pouch.put(doc).then(res => {
logger.debug('tile cached', res)
}).catch(err => {
if (err.status === 409) {
// document update conflicts may be caused by XHR flooding,
// but we can safely ignore them since we keep only one version anyway and we need no db sync
logger.debug(err)
} else {
logger.error(err)
}
this.fire('tilecacheerror', {tile: doc._id, error: err})
})
}

});

@albfan
Copy link

albfan commented Jun 28, 2023

Turning this into a diff to see the difference: I don't see the problem to create a PR

diff --git 1/L.TileLayer.PouchDBCached.js 2/new
index c0d7d5c..25a96d5 100644
--- 1/L.TileLayer.PouchDBCached.js
+++ 2/titi
@@ -1,280 +1,140 @@
-
-
 L.TileLayer.addInitHook(function () {
-
-	if (!this.options.useCache) {
-		this._db     = null;
-		this._canvas = null;
-		return;
+if (this.options.cache) {
+this.pouch = new PouchDB(tiles-${this.options.cacheName}, {
+size: 1000,
+auto_compaction: true,
+revs_limit: 1
+})
+logger.info(pouchdb: ${this.pouch.name}, adapter: ${this.pouch.adapter})
 }
+})
 
-	var dbName = this.options.dbName || 'offline-tiles';
-	if (this.options.dbOptions) {
-		this._db = new PouchDB(dbName, this.options.dbOptions);
-	} else {
-		this._db = new PouchDB(dbName);
-	}
-	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;
-	}
-});
-
-// 🍂namespace TileLayer
-// 🍂section PouchDB tile caching options
-// 🍂option useCache: Boolean = false
 // Whether to use a PouchDB cache on this tile layer, or not
-L.TileLayer.prototype.options.useCache     = false;
+L.TileLayer.prototype.options.cache = false;
 
-// 🍂option saveToCache: Boolean = true
-// When caching is enabled, whether to save new tiles to the cache or not
-L.TileLayer.prototype.options.saveToCache  = true;
-
-// 🍂option useOnlyCache: Boolean = false
-// When caching is enabled, whether to request new tiles from the network or not
-L.TileLayer.prototype.options.useOnlyCache = false;
-
-// 🍂option useCache: String = 'image/png'
-// The image format to be used when saving the tile images in the cache
-L.TileLayer.prototype.options.cacheFormat = 'image/png';
-
-// 🍂option cacheMaxAge: Number = 24*3600*1000
 // Maximum age of the cache, in milliseconds
 L.TileLayer.prototype.options.cacheMaxAge = 24 * 3600 * 1000;
 
-
 L.TileLayer.include({
 
 // Overwrites L.TileLayer.prototype.createTile
-	createTile: function(coords, done) {
-		var tile = document.createElement('img');
-
-		tile.onerror = L.bind(this._tileOnError, this, done, tile);
-
+createTile(coords, done) {
+const url = this.getTileUrl(coords);
+const tile = document.createElement('img');
 if (this.options.crossOrigin) {
 tile.crossOrigin = '';
 }
-
-		/*
-		 Alt tag is *set to empty string to keep screen readers from reading URL and for compliance reasons
-		 http://www.w3.org/TR/WCAG20-TECHS/H67
-		 */
-		tile.alt = '';
-
-		var tileUrl = this.getTileUrl(coords);
-
-		if (this.options.useCache && this._canvas) {
-			this._db.get(tileUrl, {revs_info: true}, this._onCacheLookup(tile, tileUrl, done));
+tile.onerror = () => {
+logger.error(bom.leaf load failed: ${tile.src})
+}
+if (this.options.cache) {
+this.pouch.get(url, {attachments: true, binary: true, revs_info: true}, this.onCacheLookup(tile, url, done));
 } else {
 // Fall back to standard behaviour
 tile.onload = L.bind(this._tileOnLoad, this, done, tile);
+tile.src = url;
 }
-
-		tile.src = tileUrl;
 return tile;
 },
 
-	// 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, done) {
+// Returns a callback (closure over tile/key/originalSrc) to be run when the DB backend is finished with a fetch operation.
+onCacheLookup(tile, tileUrl, done) {
 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 = L.bind(this._saveTile, this, tile, tileUrl, data._revs_info[0].rev, done);
-					}
-					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;
-					}
+this.fire('tilecachehit', {tile, url: tileUrl});
+if ((Date.now() > (data.timestamp + this.options.cacheMaxAge))) {
+// old tile, refresh it
+tile.qwest = null
+this.readAndSaveTile(tile, tileUrl, data._revs_info[0].rev, done)
 } else {
-					// Serve tile from cached data
-					//console.log('Tile is cached: ', tileUrl);
+// tile from cache
 tile.onload = L.bind(this._tileOnLoad, this, done, tile);
-					tile.src = data.dataUrl;    // data.dataUrl is already a base64-encoded PNG image.
+tile.src = URL.createObjectURL(data._attachments.img.data);
 }
 } else {
-				this.fire('tilecachemiss', {
-					tile: tile,
-					url: tileUrl
-				});
-				if (this.options.useOnlyCache) {
-					// Offline, not cached
-// 					console.log('Tile not in cache', tileUrl);
-					tile.onload = L.Util.falseFn;
-					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 = L.bind(this._saveTile, this, tile, tileUrl, null, done);
-					} else {
-						tile.onload = L.bind(this._tileOnLoad, this, done, tile);
-					}
-					tile.crossOrigin = 'Anonymous';
-					tile.src = tileUrl;
-				}
+this.fire('tilecachemiss', {tile, url: tileUrl});
+this.readAndSaveTile(tile, tileUrl, null, done)
 }
 }.bind(this);
 },
 
-	// Returns an event handler (closure over DB key), which runs
-	//   when the tile (which is an <img>) 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(tile, tileUrl, existingRevision, done) {
-		if (this._canvas === null) return;
-		this._canvas.width  = tile.naturalWidth  || tile.width;
-		this._canvas.height = tile.naturalHeight || tile.height;
-
-		var context = this._canvas.getContext('2d');
-		context.drawImage(tile, 0, 0);
-
-		var dataUrl;
-		try {
-			dataUrl = this._canvas.toDataURL(this.options.cacheFormat);
-		} catch(err) {
-			this.fire('tilecacheerror', { tile: tile, error: err });
-			return done();
+readAndSaveTile(tile, tileUrl, existingRevision, done) {
+if (tile.qwest) {
+return
 }
-
-		var doc = {_id: tileUrl, dataUrl: dataUrl, timestamp: Date.now()};
-	    if (existingRevision) {
-	      this._db.get(tileUrl).then(function(doc) {
-	          return this._db.put({
-	              _id: doc._id,
-	              _rev: doc._rev,
-	              dataUrl: dataUrl,
-	              timestamp: Date.now()
-	          });
-	      }.bind(this)).then(function(response) {
-	        //console.log('_saveTile update: ', response);
-	      });
-	    } else {
-	      this._db.put(doc).then( function(doc) {
-	        //console.log('_saveTile insert: ', doc);
-	      });
+let headers = {}
+if (tileUrl.indexOf('cors-anywhere') >= 0) {
+headers = {
+'X-Requested-With': 'XMLHttpRequest'
 }
-
-	    if (done) {
-	      done();
 }
+tile.qwest = qwest.get(tileUrl, null, {headers, cache: true, responseType: 'blob'}).then(res => {
+this.saveTile(res.response, tileUrl, existingRevision, done)
+tile.src = URL.createObjectURL(res.response);
+}).catch(error => {
+logger.error('qwest error', error.message);
+})
 },
 
-	// 🍂section PouchDB tile caching options
-	// 🍂method seed(bbox: LatLngBounds, minZoom: Number, maxZoom: Number): this
-	// Starts seeding the cache given a bounding box and the minimum/maximum zoom levels
-	// Use with care! This can spawn thousands of requests and flood tileservers!
-	seed: function(bbox, minZoom, maxZoom) {
-		if (!this.options.useCache) return;
-		if (minZoom > maxZoom) return;
-		if (!this._map) return;
-
-		var queue = [];
-
-		for (var z = minZoom; z<=maxZoom; z++) {
-
-			var northEastPoint = this._map.project(bbox.getNorthEast(),z);
-			var southWestPoint = this._map.project(bbox.getSouthWest(),z);
-
-			// Calculate tile indexes as per L.TileLayer._update and
-			//   L.TileLayer._addTilesFromCenterOut
-			var tileSize = this.getTileSize();
-			var tileBounds = L.bounds(
-				L.point(Math.floor(northEastPoint.x / tileSize.x), Math.floor(northEastPoint.y / tileSize.y)),
-				L.point(Math.floor(southWestPoint.x / tileSize.x), Math.floor(southWestPoint.y / tileSize.y)));
-
-			for (var j = tileBounds.min.y; j <= tileBounds.max.y; j++) {
-				for (var i = tileBounds.min.x; i <= tileBounds.max.x; i++) {
-					point = new L.Point(i, j);
-					point.z = z;
-					queue.push(this._getTileUrl(point));
+// Overwrite L.TileLayer.prototype._abortLoading
+_abortLoading() {
+let i
+let tile
+for (i in this._tiles) {
+if (this._tiles[i].coords.z !== this._tileZoom) {
+tile = this._tiles[i].el;
+tile.onload = L.Util.falseFn;
+tile.onerror = L.Util.falseFn;
+if (!tile.complete) {
+tile.src = L.Util.emptyImageUrl;
+L.DomUtil.remove(tile);
 }
+if (tile.qwest) {
+tile.qwest.abort()
 }
 }
-
-		var seedData = {
-			bbox: bbox,
-			minZoom: minZoom,
-			maxZoom: maxZoom,
-			queueLength: queue.length
 }
-		this.fire('seedstart', seedData);
-		var tile = this._createTile();
-		tile._layer = this;
-		this._seedOneTile(tile, queue, seedData);
-		return this;
 },
 
-	_createTile: function () {
-		return new Image();
-	},
-
-	// Modified L.TileLayer.getTileUrl, this will use the zoom given by the parameter coords
-	//  instead of the maps current zoomlevel.
-	_getTileUrl: function (coords) {
-		var zoom = coords.z;
-		if (this.options.zoomReverse) {
-			zoom = this.options.maxZoom - zoom;
+// 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(tileBlob, tileUrl, existingRevision, done) {
+const doc = {
+_id: tileUrl,
+_attachments: {
+img: {
+content_type: 'image/jpeg',
+data: tileBlob
 }
-		zoom += this.options.zoomOffset;
-		return L.Util.template(this._url, L.extend({
-			r: this.options.detectRetina && L.Browser.retina && this.options.maxZoom > 0 ? '@2x' : '',
-			s: this._getSubdomain(coords),
-			x: coords.x,
-			y: this.options.tms ? this._globalTileRange.max.y - coords.y : coords.y,
-			z: this.options.maxNativeZoom ? Math.min(zoom, this.options.maxNativeZoom) : zoom
-		}, this.options));
 },
-
-	// Uses a defined tile to eat through one item in the queue and
-	//   asynchronously recursively call itself when the tile has
-	//   finished loading.
-	_seedOneTile: function(tile, remaining, seedData) {
-		if (!remaining.length) {
-			this.fire('seedend', seedData);
-			return;
+timestamp: Date.now()
 }
-		this.fire('seedprogress', {
-			bbox:    seedData.bbox,
-			minZoom: seedData.minZoom,
-			maxZoom: seedData.maxZoom,
-			queueLength: seedData.queueLength,
-			remainingLength: remaining.length
-		});
-
-		var url = remaining.pop();
+if (existingRevision) {
+this.pouch.remove(tileUrl, existingRevision, () => {
+this.pouchPut(doc)
+})
+} else {
+this.pouchPut(doc)
+}
+if (done) {
+done()
+}
+},
 
-		this._db.get(url, function(err, data) {
-			if (!data) {
-				tile.onload = function(e) {
-					this._saveTile(tile, url, null);
-					this._seedOneTile(tile, remaining, seedData);
-				}.bind(this);
-				tile.onerror = function(e) {
-					// Could not load tile, let's continue anyways.
-					this._seedOneTile(tile, remaining, seedData);
-				}.bind(this);
-				tile.crossOrigin = 'Anonymous';
-				tile.src = url;
+pouchPut(doc) {
+this.pouch.put(doc).then(res => {
+logger.debug('tile cached', res)
+}).catch(err => {
+if (err.status === 409) {
+// document update conflicts may be caused by XHR flooding,
+// but we can safely ignore them since we keep only one version anyway and we need no db sync
+logger.debug(err)
 } else {
-				this._seedOneTile(tile, remaining, seedData);
+logger.error(err)
 }
-		}.bind(this));
+this.fire('tilecacheerror', {tile: doc._id, error: err})
+})
 }
 
 });

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants