leaflet-nogap.js (9.6 KB)


  1 // @class TileLayer
  2 
  3 L.TileLayer.mergeOptions({
  4 	// @option keepBuffer
  5 	// The amount of tiles outside the visible map area to be kept in the stitched
  6 	// `TileLayer`.
  7 
  8 	// @option dumpToCanvas: Boolean = true
  9 	// Whether to dump loaded tiles to a `<canvas>` to prevent some rendering
 10 	// artifacts. (Disabled by default in IE)
 11 	dumpToCanvas: L.Browser.canvas && !L.Browser.ie,
 12 });
 13 
 14 L.TileLayer.include({
 15 	// Backing-store density. The canvas is sized in CSS px (so Leaflet's positioning is
 16 	// unchanged) but its backing store is ratio× larger and the context is pre-scaled by
 17 	// ratio, so high-res source tiles keep their detail instead of being flattened to 1x.
 18 	// Full DPR for sharpness; the crash from large canvases is bounded by a small keepBuffer
 19 	// (see the tileLayer options in script.js) rather than by capping ratio.
 20 	_canvasRatio: function() {
 21 		return Math.max(1, Math.round(window.devicePixelRatio || 1));
 22 	},
 23 
 24 	_onUpdateLevel: function(z) {
 25 		if (this.options.dumpToCanvas) {
 26 			// base Leaflet calls this with only z; mirror its own zIndex math off _tileZoom
 27 			// (the original plugin read an undefined `zoom` arg here, yielding NaN). Correct
 28 			// z-index stacking lets a new level's canvas sit over the old until it's pruned,
 29 			// so no opacity tricks are needed to avoid a pop on zoom.
 30 			var curZoom = this._tileZoom;
 31 			this._levels[z].canvas.style.zIndex =
 32 				this.options.maxZoom - Math.abs(curZoom - z);
 33 		}
 34 	},
 35 
 36 	_onRemoveLevel: function(z) {
 37 		if (this.options.dumpToCanvas) {
 38 			L.DomUtil.remove(this._levels[z].canvas);
 39 		}
 40 	},
 41 
 42 	_onCreateLevel: function(level) {
 43 		if (this.options.dumpToCanvas) {
 44 			level.canvas = L.DomUtil.create(
 45 				"canvas",
 46 				"leaflet-tile-container leaflet-zoom-animated",
 47 				this._container
 48 			);
 49 			level.canvas.style.pointerEvents = "none";
 50 			level.ctx = level.canvas.getContext("2d");
 51 			this._resetCanvasSize(level);
 52 		}
 53 	},
 54 
 55 	_removeTile: function(key) {
 56 		if (this.options.dumpToCanvas) {
 57 			var tile = this._tiles[key];
 58 			var level = this._levels[tile.coords.z];
 59 			var tileSize = this.getTileSize();
 60 
 61 			if (level) {
 62 				// Where in the canvas should this tile go?
 63 				var offset = L.point(tile.coords.x, tile.coords.y)
 64 					.subtract(level.canvasRange.min)
 65 					.scaleBy(this.getTileSize());
 66 
 67 				level.ctx.clearRect(offset.x, offset.y, tileSize.x, tileSize.y);
 68 			}
 69 		}
 70 
 71 		L.GridLayer.prototype._removeTile.call(this, key);
 72 	},
 73 
 74 	_resetCanvasSize: function(level) {
 75 		var buff = this.options.keepBuffer,
 76 			pixelBounds = this._getTiledPixelBounds(this._map.getCenter()),
 77 			tileRange = this._pxBoundsToTileRange(pixelBounds),
 78 			tileSize = this.getTileSize();
 79 
 80 		tileRange.min = tileRange.min.subtract([buff, buff]); // This adds the no-prune buffer
 81 		tileRange.max = tileRange.max.add([buff + 1, buff + 1]);
 82 
 83 		var pixelRange = L.bounds(
 84 				tileRange.min.scaleBy(tileSize),
 85 				tileRange.max.add([1, 1]).scaleBy(tileSize) // This prevents an off-by-one when checking if tiles are inside
 86 			),
 87 			mustRepositionCanvas = false,
 88 			neededSize = pixelRange.max.subtract(pixelRange.min);
 89 
 90 		// Resize the canvas, if needed, and only to make it bigger. CSS size stays in CSS px;
 91 		// backing store is ratio× larger so high-DPR tiles keep their detail (see _canvasRatio).
 92 		var ratio = this._canvasRatio();
 93 		if (
 94 			neededSize.x > level.canvas.width / ratio ||
 95 			neededSize.y > level.canvas.height / ratio
 96 		) {
 97 			// Resizing canvases erases the currently drawn content, I'm afraid.
 98 			// To keep it, dump the pixels to another canvas, then display it on
 99 			// top. This could be done with getImageData/putImageData, but that
100 			// would break for tainted canvases (in non-CORS tilesets). Copy at
101 			// backing-store resolution (1:1, no ctx scale) to avoid resampling.
102 			var oldSize = { x: level.canvas.width, y: level.canvas.height };
103 			// console.info('Resizing canvas from ', oldSize, 'to ', neededSize);
104 
105 			var tmpCanvas = L.DomUtil.create("canvas");
106 			tmpCanvas.width = oldSize.x;
107 			tmpCanvas.height = oldSize.y;
108 			tmpCanvas.getContext("2d").drawImage(level.canvas, 0, 0);
109 			// var data = level.ctx.getImageData(0, 0, oldSize.x, oldSize.y);
110 
111 			level.canvas.style.width = neededSize.x + "px";
112 			level.canvas.style.height = neededSize.y + "px";
113 			level.canvas.width = neededSize.x * ratio;
114 			level.canvas.height = neededSize.y * ratio;
115 			// drawImage at native backing-store coords, so reset any prior ctx scale first
116 			level.ctx.setTransform(1, 0, 0, 1, 0, 0);
117 			level.ctx.drawImage(tmpCanvas, 0, 0);
118 			// every other draw in this layer works in CSS px; pre-scale so it lands on the
119 			// ratio× backing store
120 			level.ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
121 			// level.ctx.putImageData(data, 0, 0, 0, 0, oldSize.x, oldSize.y);
122 		}
123 
124 		// Translate the canvas contents if it's moved around. This is a whole-canvas
125 		// backing-store copy, so run it at native scale (offset in backing-store px) and
126 		// restore the CSS-px ctx scale afterward.
127 		if (level.canvasRange) {
128 			var offset = level.canvasRange.min
129 				.subtract(tileRange.min)
130 				.scaleBy(this.getTileSize())
131 				.multiplyBy(ratio);
132 
133 			// 			console.info('Offsetting by ', offset);
134 
135 			level.ctx.setTransform(1, 0, 0, 1, 0, 0);
136 
137 			if (!L.Browser.safari) {
138 				// By default, canvases copy things "on top of" existing pixels, but we want
139 				// this to *replace* the existing pixels when doing a drawImage() call.
140 				// This will also clear the sides, so no clearRect() calls are needed to make room
141 				// for the new tiles.
142 				level.ctx.globalCompositeOperation = "copy";
143 				level.ctx.drawImage(level.canvas, offset.x, offset.y);
144 				level.ctx.globalCompositeOperation = "source-over";
145 			} else {
146 				// Safari clears the canvas when copying from itself :-(
147 				if (!this._tmpCanvas) {
148 					var t = (this._tmpCanvas = L.DomUtil.create("canvas"));
149 					t.width = level.canvas.width;
150 					t.height = level.canvas.height;
151 					this._tmpContext = t.getContext("2d");
152 				}
153 				this._tmpContext.clearRect(
154 					0,
155 					0,
156 					level.canvas.width,
157 					level.canvas.height
158 				);
159 				this._tmpContext.drawImage(level.canvas, 0, 0);
160 				level.ctx.clearRect(0, 0, level.canvas.width, level.canvas.height);
161 				level.ctx.drawImage(this._tmpCanvas, offset.x, offset.y);
162 			}
163 
164 			level.ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
165 			mustRepositionCanvas = true; // Wait until new props are set
166 		}
167 
168 		level.canvasRange = tileRange;
169 		level.canvasPxRange = pixelRange;
170 		level.canvasOrigin = pixelRange.min;
171 
172 		// console.log('Canvas tile range: ', level, tileRange.min, tileRange.max );
173 		// console.log('Canvas pixel range: ', pixelRange.min, pixelRange.max );
174 		// console.log('Level origin: ', level.origin );
175 
176 		if (mustRepositionCanvas) {
177 			this._setCanvasZoomTransform(
178 				level,
179 				this._map.getCenter(),
180 				this._map.getZoom()
181 			);
182 		}
183 	},
184 
185 	/// set transform/position of canvas, in addition to the transform/position of the individual tile container
186 	_setZoomTransform: function(level, center, zoom) {
187 		L.GridLayer.prototype._setZoomTransform.call(this, level, center, zoom);
188 		if (this.options.dumpToCanvas) {
189 			this._setCanvasZoomTransform(level, center, zoom);
190 		}
191 	},
192 
193 	// This will get called twice:
194 	// * From _setZoomTransform
195 	// * When the canvas has shifted due to a new tile being loaded
196 	_setCanvasZoomTransform: function(level, center, zoom) {
197 		// console.log('_setCanvasZoomTransform', level, center, zoom);
198 		if (!level.canvasOrigin) {
199 			return;
200 		}
201 		var scale = this._map.getZoomScale(zoom, level.zoom),
202 			translate = level.canvasOrigin
203 				.multiplyBy(scale)
204 				.subtract(this._map._getNewPixelOrigin(center, zoom))
205 				.round();
206 
207 		if (L.Browser.any3d) {
208 			L.DomUtil.setTransform(level.canvas, translate, scale);
209 		} else {
210 			L.DomUtil.setPosition(level.canvas, translate);
211 		}
212 	},
213 
214 	_onOpaqueTile: function(tile) {
215 		if (!this.options.dumpToCanvas) {
216 			return;
217 		}
218 
219 		// Guard against an NS_ERROR_NOT_AVAILABLE (or similar) exception
220 		// when a non-image-tile has been loaded (e.g. a WMS error).
221 		// Checking for tile.el.complete is not enough, as it has been
222 		// already marked as loaded and ready somehow.
223 		try {
224 			this.dumpPixels(tile.coords, tile.el);
225 		} catch (ex) {
226 			return this.fire("tileerror", {
227 				error: "Could not copy tile pixels: " + ex,
228 				tile: tile,
229 				coods: tile.coords,
230 			});
231 		}
232 
233 		// If dumping the pixels was successful, then hide the tile.
234 		// Do not remove the tile itself, as it is needed to check if the whole
235 		// level (and its canvas) should be removed (via level.el.children.length)
236 		tile.el.style.display = "none";
237 	},
238 
239 	// @section Extension methods
240 	// @uninheritable
241 
242 	// @method dumpPixels(coords: Object, imageSource: CanvasImageSource): this
243 	// Dumps pixels from the given `CanvasImageSource` into the layer, into
244 	// the space for the tile represented by the `coords` tile coordinates (an object
245 	// like `{x: Number, y: Number, z: Number}`; the image source must have the
246 	// same size as the `tileSize` option for the layer. Has no effect if `dumpToCanvas`
247 	// is `false`.
248 	dumpPixels: function(coords, imageSource) {
249 		var level = this._levels[coords.z],
250 			tileSize = this.getTileSize();
251 
252 		if (!level.canvasRange || !this.options.dumpToCanvas) {
253 			return;
254 		}
255 
256 		// Check if the tile is inside the currently visible map bounds
257 		// There is a possible race condition when tiles are loaded after they
258 		// have been panned outside of the map.
259 		if (!level.canvasRange.contains(coords)) {
260 			this._resetCanvasSize(level);
261 		}
262 
263 		// Where in the canvas should this tile go?
264 		var offset = L.point(coords.x, coords.y)
265 			.subtract(level.canvasRange.min)
266 			.scaleBy(this.getTileSize());
267 
268 		level.ctx.drawImage(imageSource, offset.x, offset.y, tileSize.x, tileSize.y);
269 
270 		// TODO: Clear the pixels of other levels' canvases where they overlap
271 		// this newly dumped tile.
272 		return this;
273 	},
274 });