├── .eslintrc.js ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── circle.yml ├── index.html ├── index.js ├── leaflet-image.js ├── package.json ├── site.js └── test ├── out ├── circle-marker.png ├── no-tiles.png ├── one-point-oh.png ├── osm.png ├── simple.png ├── style-layer.png └── wms.png ├── pages ├── circle-marker.html ├── no-tiles.html ├── one-point-oh.html ├── osm.html ├── shared │ └── style.css ├── simple.html ├── style-layer.html └── wms.html └── test.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "rules": { 8 | "indent": [ 9 | "error", 10 | 4 11 | ], 12 | "linebreak-style": [ 13 | "error", 14 | "unix" 15 | ], 16 | "quotes": [ 17 | "error", 18 | "single" 19 | ], 20 | "semi": [ 21 | "error", 22 | "always" 23 | ] 24 | } 25 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Before you post an issue, please review this document! Many of your questions, 2 | like "why isn't leaflet-image working" or "will it work with a plugin", 3 | are answered below. 4 | 5 | ## Requirements 6 | 7 | * Tile layer providers (OSM, MapBox, etc) must support [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) 8 | * Any markers on the map must also support CORS. The default Leaflet-CDN markers 9 | don't, so they aren't supported. 10 | * Your browser must support [CORS](http://caniuse.com/#feat=cors) and [Canvas](http://caniuse.com/#feat=canvas), 11 | so `IE >= 10` with no exceptions. 12 | * This library **does not rasterize HTML** because **browsers cannot rasterize HTML**. Therefore, 13 | L.divIcon and other HTML-based features of a map, like zoom controls or legends, are not 14 | included in the output, because they are HTML. 15 | 16 | __For Leaflet < 1.0.0__: You must set `L_PREFER_CANVAS = true;` so that vector 17 | layers are drawn in Canvas 18 | 19 | __For Leaflet >= 1.0.0__: You must set `renderer: L.canvas()` for any layer that 20 | you want included in the generated image. You can also set this by setting [`preferCanvas: true`](http://leafletjs.com/reference-1.0.0.html#map-prefercanvas) in your map's options. 21 | 22 | ## Plugins that will _not_ work with leaflet-image 23 | 24 | * Leaflet.label: will not work because it uses HTML to display labels. 25 | * Leaflet.markercluster: will not work because it uses HTML for clusters. 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | BSD 2-Clause License 3 | 4 | Copyright (c) 2017, Mapbox 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## leaflet-image 2 | 3 | [![CircleCI](https://circleci.com/gh/mapbox/leaflet-image/tree/gh-pages.svg?style=svg)](https://circleci.com/gh/mapbox/leaflet-image/tree/gh-pages) 4 | 5 | Export images out of Leaflet maps without a server component, by using 6 | Canvas and [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing). 7 | 8 | ## Requirements 9 | 10 | * Tile layer providers (OSM, MapBox, etc) must support [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) 11 | * Any markers on the map must also support CORS. The default Leaflet-CDN markers 12 | don't, so they aren't supported. 13 | * Your browser must support [CORS](http://caniuse.com/#feat=cors) and [Canvas](http://caniuse.com/#feat=canvas), 14 | so `IE >= 10` with no exceptions. 15 | * This library **does not rasterize HTML** because **browsers cannot rasterize HTML**. Therefore, 16 | L.divIcon and other HTML-based features of a map, like zoom controls or legends, are not 17 | included in the output, because they are HTML. 18 | 19 | __For Leaflet < 1.0.0__: You must set `L_PREFER_CANVAS = true;` so that vector 20 | layers are drawn in Canvas 21 | 22 | __For Leaflet >= 1.0.0__: You must set `renderer: L.canvas()` for any layer that 23 | you want included in the generated image. You can also set this by setting [`preferCanvas: true`](http://leafletjs.com/reference-1.0.0.html#map-prefercanvas) in your map's options. 24 | 25 | ## Plugins that will _not_ work with leaflet-image 26 | 27 | * Leaflet.label: will not work because it uses HTML to display labels. 28 | * Leaflet.markercluster: will not work because it uses HTML for clusters. 29 | 30 | ### Usage 31 | 32 | browserify 33 | 34 | npm install --save leaflet-image 35 | 36 | web 37 | 38 | curl -L https://unpkg.com/leaflet-image@latest/leaflet-image.js > leaflet-image.js 39 | 40 | ### Example 41 | 42 | ```js 43 | var map = L.mapbox.map('map', 'YOUR.MAPID').setView([38.9, -77.03], 14); 44 | leafletImage(map, function(err, canvas) { 45 | // now you have canvas 46 | // example thing to do with that canvas: 47 | var img = document.createElement('img'); 48 | var dimensions = map.getSize(); 49 | img.width = dimensions.x; 50 | img.height = dimensions.y; 51 | img.src = canvas.toDataURL(); 52 | document.getElementById('images').innerHTML = ''; 53 | document.getElementById('images').appendChild(img); 54 | }); 55 | ``` 56 | 57 | ### Plugin CDN 58 | 59 | leaflet-image is [available through the Mapbox Plugin CDN](https://www.mapbox.com/mapbox.js/plugins/#leaflet-image) so you don't need to download & copy it. Just include the following script tag: 60 | 61 | ```html 62 | 63 | ``` 64 | 65 | ### API 66 | 67 | ```js 68 | leafletImage(map, callback) 69 | ``` 70 | 71 | map is a `L.map` or `L.mapbox.map`, callback takes `(err, canvas)`. 72 | 73 | ## Attribution 74 | 75 | Any images you generate from maps that require attribution - which is most, including all from commercial sources and those that include any data from OpenStreetMap - will require the same attribution as the map did. Remember to attribute. 76 | 77 | ## See Also 78 | 79 | * The [Mapbox Static Image API](https://www.mapbox.com/developers/api/static/) is simpler to use 80 | and faster than this approach. 81 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 5 4 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Leaflet Image 6 | 7 | 8 | 9 | 13 | 14 | 15 | 31 | 32 | 33 |
34 |
35 | 36 | 37 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* global L */ 2 | 3 | var queue = require('d3-queue').queue; 4 | 5 | var cacheBusterDate = +new Date(); 6 | 7 | // leaflet-image 8 | module.exports = function leafletImage(map, callback) { 9 | 10 | var hasMapbox = !!L.mapbox; 11 | 12 | var dimensions = map.getSize(), 13 | layerQueue = new queue(1); 14 | 15 | var canvas = document.createElement('canvas'); 16 | canvas.width = dimensions.x; 17 | canvas.height = dimensions.y; 18 | var ctx = canvas.getContext('2d'); 19 | 20 | // dummy canvas image when loadTile get 404 error 21 | // and layer don't have errorTileUrl 22 | var dummycanvas = document.createElement('canvas'); 23 | dummycanvas.width = 1; 24 | dummycanvas.height = 1; 25 | var dummyctx = dummycanvas.getContext('2d'); 26 | dummyctx.fillStyle = 'rgba(0,0,0,0)'; 27 | dummyctx.fillRect(0, 0, 1, 1); 28 | 29 | // layers are drawn in the same order as they are composed in the DOM: 30 | // tiles, paths, and then markers 31 | map.eachLayer(drawTileLayer); 32 | map.eachLayer(drawEsriDynamicLayer); 33 | 34 | if (map._pathRoot) { 35 | layerQueue.defer(handlePathRoot, map._pathRoot); 36 | } else if (map._panes) { 37 | var firstCanvas = map._panes.overlayPane.getElementsByTagName('canvas').item(0); 38 | if (firstCanvas) { layerQueue.defer(handlePathRoot, firstCanvas); } 39 | } 40 | map.eachLayer(drawMarkerLayer); 41 | layerQueue.awaitAll(layersDone); 42 | 43 | function drawTileLayer(l) { 44 | if (l instanceof L.TileLayer) layerQueue.defer(handleTileLayer, l); 45 | else if (l._heat) layerQueue.defer(handlePathRoot, l._canvas); 46 | } 47 | 48 | function drawMarkerLayer(l) { 49 | if (l instanceof L.Marker && l.options.icon instanceof L.Icon) { 50 | layerQueue.defer(handleMarkerLayer, l); 51 | } 52 | } 53 | 54 | function drawEsriDynamicLayer(l) { 55 | if (!L.esri) return; 56 | 57 | if (l instanceof L.esri.DynamicMapLayer) { 58 | layerQueue.defer(handleEsriDymamicLayer, l); 59 | } 60 | } 61 | 62 | function done() { 63 | callback(null, canvas); 64 | } 65 | 66 | function layersDone(err, layers) { 67 | if (err) throw err; 68 | layers.forEach(function (layer) { 69 | if (layer && layer.canvas) { 70 | ctx.drawImage(layer.canvas, 0, 0); 71 | } 72 | }); 73 | done(); 74 | } 75 | 76 | function handleTileLayer(layer, callback) { 77 | // `L.TileLayer.Canvas` was removed in leaflet 1.0 78 | var isCanvasLayer = (L.TileLayer.Canvas && layer instanceof L.TileLayer.Canvas), 79 | canvas = document.createElement('canvas'); 80 | 81 | canvas.width = dimensions.x; 82 | canvas.height = dimensions.y; 83 | 84 | var ctx = canvas.getContext('2d'), 85 | bounds = map.getPixelBounds(), 86 | zoom = map.getZoom(), 87 | tileSize = layer.options.tileSize; 88 | 89 | if (zoom > layer.options.maxZoom || 90 | zoom < layer.options.minZoom || 91 | // mapbox.tileLayer 92 | (hasMapbox && 93 | layer instanceof L.mapbox.tileLayer && !layer.options.tiles)) { 94 | return callback(); 95 | } 96 | 97 | var tileBounds = L.bounds( 98 | bounds.min.divideBy(tileSize)._floor(), 99 | bounds.max.divideBy(tileSize)._floor()), 100 | tiles = [], 101 | j, i, 102 | tileQueue = new queue(1); 103 | 104 | for (j = tileBounds.min.y; j <= tileBounds.max.y; j++) { 105 | for (i = tileBounds.min.x; i <= tileBounds.max.x; i++) { 106 | tiles.push(new L.Point(i, j)); 107 | } 108 | } 109 | 110 | tiles.forEach(function (tilePoint) { 111 | var originalTilePoint = tilePoint.clone(); 112 | 113 | if (layer._adjustTilePoint) { 114 | layer._adjustTilePoint(tilePoint); 115 | } 116 | 117 | var tilePos = originalTilePoint 118 | .scaleBy(new L.Point(tileSize, tileSize)) 119 | .subtract(bounds.min); 120 | 121 | if (tilePoint.y >= 0) { 122 | if (isCanvasLayer) { 123 | var tile = layer._tiles[tilePoint.x + ':' + tilePoint.y]; 124 | tileQueue.defer(canvasTile, tile, tilePos, tileSize); 125 | } else { 126 | var url = addCacheString(layer.getTileUrl(tilePoint)); 127 | tileQueue.defer(loadTile, url, tilePos, tileSize); 128 | } 129 | } 130 | }); 131 | 132 | tileQueue.awaitAll(tileQueueFinish); 133 | 134 | function canvasTile(tile, tilePos, tileSize, callback) { 135 | callback(null, { 136 | img: tile, 137 | pos: tilePos, 138 | size: tileSize 139 | }); 140 | } 141 | 142 | function loadTile(url, tilePos, tileSize, callback) { 143 | var im = new Image(); 144 | im.crossOrigin = ''; 145 | im.onload = function () { 146 | callback(null, { 147 | img: this, 148 | pos: tilePos, 149 | size: tileSize 150 | }); 151 | }; 152 | im.onerror = function (e) { 153 | // use canvas instead of errorTileUrl if errorTileUrl get 404 154 | if (layer.options.errorTileUrl != '' && e.target.errorCheck === undefined) { 155 | e.target.errorCheck = true; 156 | e.target.src = layer.options.errorTileUrl; 157 | } else { 158 | callback(null, { 159 | img: dummycanvas, 160 | pos: tilePos, 161 | size: tileSize 162 | }); 163 | } 164 | }; 165 | im.src = url; 166 | } 167 | 168 | function tileQueueFinish(err, data) { 169 | data.forEach(drawTile); 170 | callback(null, { canvas: canvas }); 171 | } 172 | 173 | function drawTile(d) { 174 | ctx.drawImage(d.img, Math.floor(d.pos.x), Math.floor(d.pos.y), 175 | d.size, d.size); 176 | } 177 | } 178 | 179 | function handlePathRoot(root, callback) { 180 | var bounds = map.getPixelBounds(), 181 | origin = map.getPixelOrigin(), 182 | canvas = document.createElement('canvas'); 183 | canvas.width = dimensions.x; 184 | canvas.height = dimensions.y; 185 | var ctx = canvas.getContext('2d'); 186 | var pos = L.DomUtil.getPosition(root).subtract(bounds.min).add(origin); 187 | try { 188 | ctx.drawImage(root, pos.x, pos.y, canvas.width - (pos.x * 2), canvas.height - (pos.y * 2)); 189 | callback(null, { 190 | canvas: canvas 191 | }); 192 | } catch(e) { 193 | console.error('Element could not be drawn on canvas', root); // eslint-disable-line no-console 194 | } 195 | } 196 | 197 | function handleMarkerLayer(marker, callback) { 198 | var canvas = document.createElement('canvas'), 199 | ctx = canvas.getContext('2d'), 200 | pixelBounds = map.getPixelBounds(), 201 | minPoint = new L.Point(pixelBounds.min.x, pixelBounds.min.y), 202 | pixelPoint = map.project(marker.getLatLng()), 203 | isBase64 = /^data\:/.test(marker._icon.src), 204 | url = isBase64 ? marker._icon.src : addCacheString(marker._icon.src), 205 | im = new Image(), 206 | options = marker.options.icon.options, 207 | size = options.iconSize, 208 | pos = pixelPoint.subtract(minPoint), 209 | anchor = L.point(options.iconAnchor || size && size.divideBy(2, true)); 210 | 211 | if (size instanceof L.Point) size = [size.x, size.y]; 212 | 213 | var x = Math.round(pos.x - size[0] + anchor.x), 214 | y = Math.round(pos.y - anchor.y); 215 | 216 | canvas.width = dimensions.x; 217 | canvas.height = dimensions.y; 218 | im.crossOrigin = ''; 219 | 220 | im.onload = function () { 221 | ctx.drawImage(this, x, y, size[0], size[1]); 222 | callback(null, { 223 | canvas: canvas 224 | }); 225 | }; 226 | 227 | im.src = url; 228 | 229 | if (isBase64) im.onload(); 230 | } 231 | 232 | function handleEsriDymamicLayer(dynamicLayer, callback) { 233 | var canvas = document.createElement('canvas'); 234 | canvas.width = dimensions.x; 235 | canvas.height = dimensions.y; 236 | 237 | var ctx = canvas.getContext('2d'); 238 | 239 | var im = new Image(); 240 | im.crossOrigin = ''; 241 | im.src = addCacheString(dynamicLayer._currentImage._image.src); 242 | 243 | im.onload = function() { 244 | ctx.drawImage(im, 0, 0); 245 | callback(null, { 246 | canvas: canvas 247 | }); 248 | }; 249 | } 250 | 251 | function addCacheString(url) { 252 | // workaround for https://github.com/mapbox/leaflet-image/issues/84 253 | if (!url) return url; 254 | // If it's a data URL we don't want to touch this. 255 | if (isDataURL(url) || url.indexOf('mapbox.com/styles/v1') !== -1) { 256 | return url; 257 | } 258 | return url + ((url.match(/\?/)) ? '&' : '?') + 'cache=' + cacheBusterDate; 259 | } 260 | 261 | function isDataURL(url) { 262 | var dataURLRegex = /^\s*data:([a-z]+\/[a-z]+(;[a-z\-]+\=[a-z\-]+)?)?(;base64)?,[a-z0-9\!\$\&\'\,\(\)\*\+\,\;\=\-\.\_\~\:\@\/\?\%\s]*\s*$/i; 263 | return !!url.match(dataURLRegex); 264 | } 265 | 266 | }; 267 | -------------------------------------------------------------------------------- /leaflet-image.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.leafletImage = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o layer.options.maxZoom || 91 | zoom < layer.options.minZoom || 92 | // mapbox.tileLayer 93 | (hasMapbox && 94 | layer instanceof L.mapbox.tileLayer && !layer.options.tiles)) { 95 | return callback(); 96 | } 97 | 98 | var tileBounds = L.bounds( 99 | bounds.min.divideBy(tileSize)._floor(), 100 | bounds.max.divideBy(tileSize)._floor()), 101 | tiles = [], 102 | j, i, 103 | tileQueue = new queue(1); 104 | 105 | for (j = tileBounds.min.y; j <= tileBounds.max.y; j++) { 106 | for (i = tileBounds.min.x; i <= tileBounds.max.x; i++) { 107 | tiles.push(new L.Point(i, j)); 108 | } 109 | } 110 | 111 | tiles.forEach(function (tilePoint) { 112 | var originalTilePoint = tilePoint.clone(); 113 | 114 | if (layer._adjustTilePoint) { 115 | layer._adjustTilePoint(tilePoint); 116 | } 117 | 118 | var tilePos = originalTilePoint 119 | .scaleBy(new L.Point(tileSize, tileSize)) 120 | .subtract(bounds.min); 121 | 122 | if (tilePoint.y >= 0) { 123 | if (isCanvasLayer) { 124 | var tile = layer._tiles[tilePoint.x + ':' + tilePoint.y]; 125 | tileQueue.defer(canvasTile, tile, tilePos, tileSize); 126 | } else { 127 | var url = addCacheString(layer.getTileUrl(tilePoint)); 128 | tileQueue.defer(loadTile, url, tilePos, tileSize); 129 | } 130 | } 131 | }); 132 | 133 | tileQueue.awaitAll(tileQueueFinish); 134 | 135 | function canvasTile(tile, tilePos, tileSize, callback) { 136 | callback(null, { 137 | img: tile, 138 | pos: tilePos, 139 | size: tileSize 140 | }); 141 | } 142 | 143 | function loadTile(url, tilePos, tileSize, callback) { 144 | var im = new Image(); 145 | im.crossOrigin = ''; 146 | im.onload = function () { 147 | callback(null, { 148 | img: this, 149 | pos: tilePos, 150 | size: tileSize 151 | }); 152 | }; 153 | im.onerror = function (e) { 154 | // use canvas instead of errorTileUrl if errorTileUrl get 404 155 | if (layer.options.errorTileUrl != '' && e.target.errorCheck === undefined) { 156 | e.target.errorCheck = true; 157 | e.target.src = layer.options.errorTileUrl; 158 | } else { 159 | callback(null, { 160 | img: dummycanvas, 161 | pos: tilePos, 162 | size: tileSize 163 | }); 164 | } 165 | }; 166 | im.src = url; 167 | } 168 | 169 | function tileQueueFinish(err, data) { 170 | data.forEach(drawTile); 171 | callback(null, { canvas: canvas }); 172 | } 173 | 174 | function drawTile(d) { 175 | ctx.drawImage(d.img, Math.floor(d.pos.x), Math.floor(d.pos.y), 176 | d.size, d.size); 177 | } 178 | } 179 | 180 | function handlePathRoot(root, callback) { 181 | var bounds = map.getPixelBounds(), 182 | origin = map.getPixelOrigin(), 183 | canvas = document.createElement('canvas'); 184 | canvas.width = dimensions.x; 185 | canvas.height = dimensions.y; 186 | var ctx = canvas.getContext('2d'); 187 | var pos = L.DomUtil.getPosition(root).subtract(bounds.min).add(origin); 188 | try { 189 | ctx.drawImage(root, pos.x, pos.y, canvas.width - (pos.x * 2), canvas.height - (pos.y * 2)); 190 | callback(null, { 191 | canvas: canvas 192 | }); 193 | } catch(e) { 194 | console.error('Element could not be drawn on canvas', root); // eslint-disable-line no-console 195 | } 196 | } 197 | 198 | function handleMarkerLayer(marker, callback) { 199 | var canvas = document.createElement('canvas'), 200 | ctx = canvas.getContext('2d'), 201 | pixelBounds = map.getPixelBounds(), 202 | minPoint = new L.Point(pixelBounds.min.x, pixelBounds.min.y), 203 | pixelPoint = map.project(marker.getLatLng()), 204 | isBase64 = /^data\:/.test(marker._icon.src), 205 | url = isBase64 ? marker._icon.src : addCacheString(marker._icon.src), 206 | im = new Image(), 207 | options = marker.options.icon.options, 208 | size = options.iconSize, 209 | pos = pixelPoint.subtract(minPoint), 210 | anchor = L.point(options.iconAnchor || size && size.divideBy(2, true)); 211 | 212 | if (size instanceof L.Point) size = [size.x, size.y]; 213 | 214 | var x = Math.round(pos.x - size[0] + anchor.x), 215 | y = Math.round(pos.y - anchor.y); 216 | 217 | canvas.width = dimensions.x; 218 | canvas.height = dimensions.y; 219 | im.crossOrigin = ''; 220 | 221 | im.onload = function () { 222 | ctx.drawImage(this, x, y, size[0], size[1]); 223 | callback(null, { 224 | canvas: canvas 225 | }); 226 | }; 227 | 228 | im.src = url; 229 | 230 | if (isBase64) im.onload(); 231 | } 232 | 233 | function handleEsriDymamicLayer(dynamicLayer, callback) { 234 | var canvas = document.createElement('canvas'); 235 | canvas.width = dimensions.x; 236 | canvas.height = dimensions.y; 237 | 238 | var ctx = canvas.getContext('2d'); 239 | 240 | var im = new Image(); 241 | im.crossOrigin = ''; 242 | im.src = addCacheString(dynamicLayer._currentImage._image.src); 243 | 244 | im.onload = function() { 245 | ctx.drawImage(im, 0, 0); 246 | callback(null, { 247 | canvas: canvas 248 | }); 249 | }; 250 | } 251 | 252 | function addCacheString(url) { 253 | // If it's a data URL we don't want to touch this. 254 | if (isDataURL(url) || url.indexOf('mapbox.com/styles/v1') !== -1) { 255 | return url; 256 | } 257 | return url + ((url.match(/\?/)) ? '&' : '?') + 'cache=' + cacheBusterDate; 258 | } 259 | 260 | function isDataURL(url) { 261 | var dataURLRegex = /^\s*data:([a-z]+\/[a-z]+(;[a-z\-]+\=[a-z\-]+)?)?(;base64)?,[a-z0-9\!\$\&\'\,\(\)\*\+\,\;\=\-\.\_\~\:\@\/\?\%\s]*\s*$/i; 262 | return !!url.match(dataURLRegex); 263 | } 264 | 265 | }; 266 | 267 | },{"d3-queue":2}],2:[function(require,module,exports){ 268 | (function (global, factory) { 269 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 270 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 271 | (factory((global.d3_queue = global.d3_queue || {}))); 272 | }(this, function (exports) { 'use strict'; 273 | 274 | var version = "2.0.3"; 275 | 276 | var slice = [].slice; 277 | 278 | var noabort = {}; 279 | 280 | function Queue(size) { 281 | if (!(size >= 1)) throw new Error; 282 | this._size = size; 283 | this._call = 284 | this._error = null; 285 | this._tasks = []; 286 | this._data = []; 287 | this._waiting = 288 | this._active = 289 | this._ended = 290 | this._start = 0; // inside a synchronous task callback? 291 | } 292 | 293 | Queue.prototype = queue.prototype = { 294 | constructor: Queue, 295 | defer: function(callback) { 296 | if (typeof callback !== "function" || this._call) throw new Error; 297 | if (this._error != null) return this; 298 | var t = slice.call(arguments, 1); 299 | t.push(callback); 300 | ++this._waiting, this._tasks.push(t); 301 | poke(this); 302 | return this; 303 | }, 304 | abort: function() { 305 | if (this._error == null) abort(this, new Error("abort")); 306 | return this; 307 | }, 308 | await: function(callback) { 309 | if (typeof callback !== "function" || this._call) throw new Error; 310 | this._call = function(error, results) { callback.apply(null, [error].concat(results)); }; 311 | maybeNotify(this); 312 | return this; 313 | }, 314 | awaitAll: function(callback) { 315 | if (typeof callback !== "function" || this._call) throw new Error; 316 | this._call = callback; 317 | maybeNotify(this); 318 | return this; 319 | } 320 | }; 321 | 322 | function poke(q) { 323 | if (!q._start) try { start(q); } // let the current task complete 324 | catch (e) { if (q._tasks[q._ended + q._active - 1]) abort(q, e); } // task errored synchronously 325 | } 326 | 327 | function start(q) { 328 | while (q._start = q._waiting && q._active < q._size) { 329 | var i = q._ended + q._active, 330 | t = q._tasks[i], 331 | j = t.length - 1, 332 | c = t[j]; 333 | t[j] = end(q, i); 334 | --q._waiting, ++q._active; 335 | t = c.apply(null, t); 336 | if (!q._tasks[i]) continue; // task finished synchronously 337 | q._tasks[i] = t || noabort; 338 | } 339 | } 340 | 341 | function end(q, i) { 342 | return function(e, r) { 343 | if (!q._tasks[i]) return; // ignore multiple callbacks 344 | --q._active, ++q._ended; 345 | q._tasks[i] = null; 346 | if (q._error != null) return; // ignore secondary errors 347 | if (e != null) { 348 | abort(q, e); 349 | } else { 350 | q._data[i] = r; 351 | if (q._waiting) poke(q); 352 | else maybeNotify(q); 353 | } 354 | }; 355 | } 356 | 357 | function abort(q, e) { 358 | var i = q._tasks.length, t; 359 | q._error = e; // ignore active callbacks 360 | q._data = undefined; // allow gc 361 | q._waiting = NaN; // prevent starting 362 | 363 | while (--i >= 0) { 364 | if (t = q._tasks[i]) { 365 | q._tasks[i] = null; 366 | if (t.abort) try { t.abort(); } 367 | catch (e) { /* ignore */ } 368 | } 369 | } 370 | 371 | q._active = NaN; // allow notification 372 | maybeNotify(q); 373 | } 374 | 375 | function maybeNotify(q) { 376 | if (!q._active && q._call) q._call(q._error, q._data); 377 | } 378 | 379 | function queue(concurrency) { 380 | return new Queue(arguments.length ? +concurrency : Infinity); 381 | } 382 | 383 | exports.version = version; 384 | exports.queue = queue; 385 | 386 | })); 387 | },{}]},{},[1])(1) 388 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet-image", 3 | "version": "0.4.0", 4 | "description": "export leaflet maps as images", 5 | "main": "index.js", 6 | "scripts": { 7 | "make": "browserify -s leafletImage index.js > leaflet-image.js", 8 | "test": "eslint index.js && tape test/test.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/mapbox/leaflet-image.git" 13 | }, 14 | "keywords": [ 15 | "leaflet", 16 | "map", 17 | "image", 18 | "export", 19 | "canvas" 20 | ], 21 | "author": "Tom MacWright", 22 | "license": "BSD-2-Clause", 23 | "bugs": { 24 | "url": "https://github.com/mapbox/leaflet-image/issues" 25 | }, 26 | "devDependencies": { 27 | "browserify": "~13.0.1", 28 | "concat-stream": "1.5.1", 29 | "eslint": "2.10.2", 30 | "pageres": "4.1.2", 31 | "pixelmatch": "4.0.1", 32 | "tape": "4.5.1" 33 | }, 34 | "dependencies": { 35 | "d3-queue": "2.0.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /site.js: -------------------------------------------------------------------------------- 1 | var map = L.mapbox.map('map', 'tmcw.map-u4ca5hnt', { 2 | tileLayer: { 3 | detectRetina: true 4 | } 5 | }) 6 | .setView([38.9, -77.03], 14); 7 | 8 | map.addLayer(L.geoJson( 9 | { 10 | "type": "FeatureCollection", 11 | "features": [ 12 | { 13 | "type": "Feature", 14 | "properties": {}, 15 | "geometry": { 16 | "type": "Point", 17 | "coordinates": [ 18 | -77.03330039978026, 19 | 38.904593064536805 20 | ] 21 | } 22 | }, 23 | { 24 | "type": "Feature", 25 | "properties": {}, 26 | "geometry": { 27 | "type": "Polygon", 28 | "coordinates": [ 29 | [ 30 | [ 31 | -77.03330039978026, 32 | 38.904593064536805 33 | ], 34 | [ 35 | -77.03330039978026, 36 | 38.906463238984344 37 | ], 38 | [ 39 | -77.03046798706055, 40 | 38.906463238984344 41 | ], 42 | [ 43 | -77.03046798706055, 44 | 38.904593064536805 45 | ], 46 | [ 47 | -77.03330039978026, 48 | 38.904593064536805 49 | ] 50 | ] 51 | ] 52 | } 53 | }, 54 | { 55 | "type": "Feature", 56 | "properties": {}, 57 | "geometry": { 58 | "type": "Polygon", 59 | "coordinates": [ 60 | [ 61 | [ 62 | -77.03725934028625, 63 | 38.896919824235354 64 | ], 65 | [ 66 | -77.0367443561554, 67 | 38.897621221219744 68 | ], 69 | [ 70 | -77.03586459159851, 71 | 38.89731227340173 72 | ], 73 | [ 74 | -77.03576803207397, 75 | 38.89664427352471 76 | ], 77 | [ 78 | -77.03648686408997, 79 | 38.896485622630514 80 | ], 81 | [ 82 | -77.03725934028625, 83 | 38.896919824235354 84 | ] 85 | ] 86 | ] 87 | } 88 | }, 89 | { 90 | "type": "Feature", 91 | "properties": {}, 92 | "geometry": { 93 | "type": "LineString", 94 | "coordinates": [ 95 | [ 96 | -77.03943729400635, 97 | 38.893454487476816 98 | ], 99 | [ 100 | -77.03924417495726, 101 | 38.895592160976626 102 | ], 103 | [ 104 | -77.03802108764648, 105 | 38.896510672795266 106 | ], 107 | [ 108 | -77.03686237335205, 109 | 38.89549195896866 110 | ], 111 | [ 112 | -77.0360255241394, 113 | 38.897045074204925 114 | ], 115 | [ 116 | -77.03536033630371, 117 | 38.89597626736416 118 | ], 119 | [ 120 | -77.03484535217285, 121 | 38.89696157424977 122 | ], 123 | [ 124 | -77.03415870666504, 125 | 38.896176669872084 126 | ] 127 | ] 128 | } 129 | } 130 | ] 131 | } 132 | )); 133 | 134 | document.getElementById('output').addEventListener('click', function() { 135 | leafletImage(map, doImage); 136 | }); 137 | 138 | window.setTimeout(function() { 139 | map.panBy([100, 100]); 140 | // map.setView([0, 0], 2); 141 | window.setTimeout(function() { 142 | leafletImage(map, doImage); 143 | }, 1000); 144 | }, 1000); 145 | 146 | function doImage(err, canvas) { 147 | var img = document.createElement('img'); 148 | var dimensions = map.getSize(); 149 | img.width = dimensions.x; 150 | img.height = dimensions.y; 151 | img.src = canvas.toDataURL(); 152 | document.getElementById('images').innerHTML = ''; 153 | document.getElementById('images').appendChild(img); 154 | } 155 | -------------------------------------------------------------------------------- /test/out/circle-marker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/leaflet-image/285edd3bf4d28135b8db011ffd129b3187b78de7/test/out/circle-marker.png -------------------------------------------------------------------------------- /test/out/no-tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/leaflet-image/285edd3bf4d28135b8db011ffd129b3187b78de7/test/out/no-tiles.png -------------------------------------------------------------------------------- /test/out/one-point-oh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/leaflet-image/285edd3bf4d28135b8db011ffd129b3187b78de7/test/out/one-point-oh.png -------------------------------------------------------------------------------- /test/out/osm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/leaflet-image/285edd3bf4d28135b8db011ffd129b3187b78de7/test/out/osm.png -------------------------------------------------------------------------------- /test/out/simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/leaflet-image/285edd3bf4d28135b8db011ffd129b3187b78de7/test/out/simple.png -------------------------------------------------------------------------------- /test/out/style-layer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/leaflet-image/285edd3bf4d28135b8db011ffd129b3187b78de7/test/out/style-layer.png -------------------------------------------------------------------------------- /test/out/wms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/leaflet-image/285edd3bf4d28135b8db011ffd129b3187b78de7/test/out/wms.png -------------------------------------------------------------------------------- /test/pages/circle-marker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/pages/no-tiles.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/pages/one-point-oh.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/pages/osm.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/pages/shared/style.css: -------------------------------------------------------------------------------- 1 | body { margin:0; padding:0; } 2 | #map { position:absolute; top:0; bottom:0; width:100%; } 3 | #map { width:50%; } 4 | #snapshot { 5 | position:absolute; 6 | top:0;bottom:0;right:0; 7 | width:50%; 8 | } 9 | -------------------------------------------------------------------------------- /test/pages/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 | 10 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/pages/style-layer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/pages/wms.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var concat = require('concat-stream'); 3 | var test = require('tape'); 4 | var pixelmatch = require('pixelmatch'); 5 | var fs = require('fs'); 6 | var Pageres = require('pageres'); 7 | 8 | ['simple', 'circle-marker', 'osm', 'one-point-oh', 9 | 'style-layer', 'no-tiles', 'wms'].forEach(name => { 10 | test(name, t => { 11 | const pageres = new Pageres({ delay: 5 }) 12 | .src(path.join(__dirname, 'pages/', name + '.html'), ['400x400']) 13 | .run() 14 | .then(res => { 15 | res[0].pipe(concat(function(buf) { 16 | if (process.env.UPDATE) { 17 | fs.writeFileSync(path.join(__dirname, 'out', name + '.png'), buf); 18 | } 19 | var expected = fs.readFileSync(path.join(__dirname, 'out/', name + '.png')); 20 | t.equal(pixelmatch(expected, buf), 0, 'image matches'); 21 | t.end(); 22 | })); 23 | }, err => { 24 | t.ifError(err); 25 | }); 26 | }); 27 | }); 28 | --------------------------------------------------------------------------------