├── .coveralls.yml ├── .eslintrc ├── .gitignore ├── .travis.yml ├── API.md ├── LICENSE ├── README.md ├── bin └── geojson-thumbnail.js ├── index.js ├── lib ├── blend.js ├── renderparams.js ├── sources.js ├── styles.js ├── template.js ├── thumbnail.js └── zoom.js ├── package-lock.json ├── package.json ├── styles ├── black.mss ├── black.xml └── default.xml └── test ├── fixtures ├── building.geojson ├── peak.geojson ├── road.geojson └── water.geojson ├── index.test.js └── zoom.test.js /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "node": true, 5 | "es6": true 6 | }, 7 | "plugins": [ 8 | "node" 9 | ], 10 | "rules": { 11 | "no-console":0, 12 | "arrow-parens": ["error", "always"], 13 | "no-var": "error", 14 | "prefer-const": "error", 15 | "array-bracket-spacing": ["error", "never"], 16 | "comma-dangle": ["error", "never"], 17 | "computed-property-spacing": ["error", "never"], 18 | "eol-last": "error", 19 | "eqeqeq": ["error", "smart"], 20 | "indent": ["error", 2, { "SwitchCase": 1 }], 21 | "no-extend-native": "error", 22 | "no-mixed-spaces-and-tabs": "error", 23 | "no-spaced-func": "error", 24 | "no-trailing-spaces": "error", 25 | "no-unused-vars": "error", 26 | "no-use-before-define": ["error", "nofunc"], 27 | "object-curly-spacing": ["error", "always"], 28 | "quotes": ["error", "single", "avoid-escape"], 29 | "semi": ["error", "always"], 30 | "space-infix-ops": "error", 31 | "spaced-comment": ["error", "always"], 32 | "keyword-spacing": ["error", { "before": true, "after": true }], 33 | "template-curly-spacing": ["error", "never"], 34 | "semi-spacing": "error", 35 | "strict": "error", 36 | "node/no-unsupported-features": ["error", { "version": 4 }], 37 | "node/no-missing-require": "error" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - '6' 5 | before_install: 6 | - npm install coveralls 7 | - npm install npm@5.6 -g 8 | addons: 9 | apt: 10 | sources: 11 | - ubuntu-toolchain-r-test 12 | packages: 13 | - g++-4.8 14 | - libstdc++6 15 | after_script: 16 | - npm run coverage 17 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Table of Contents 4 | 5 | - [renderThumbnail](#renderthumbnail) 6 | - [mapboxStreets](#mapboxstreets) 7 | - [mapboxSatellite](#mapboxsatellite) 8 | - [naturalEarth](#naturalearth) 9 | - [default](#default) 10 | - [tileDistance](#tiledistance) 11 | - [diagonalDistance](#diagonaldistance) 12 | - [tileZoomBboxFits](#tilezoombboxfits) 13 | - [decideZoom](#decidezoom) 14 | 15 | ## renderThumbnail 16 | 17 | [index.js:72-104](https://github.com/mapbox/geojson-thumbnail/blob/662e859467c2a460445814abf57fc9f534010a89/index.js#L72-L104 "Source code on GitHub") 18 | 19 | Render a thumbnmail from a GeoJSON feature 20 | 21 | **Parameters** 22 | 23 | - `geojson` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** GeoJSON Feature or FeatureCollection 24 | - `callback` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Callback called with rendered imageonce finished 25 | - `options` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** 26 | - `options.backgroundTileJSON` **[Object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)?** Provide a custom TileJSON for the background layer 27 | - `options.blendFormat` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** Format to use when blended together with the background image. 28 | 29 | ## mapboxStreets 30 | 31 | [lib/sources.js:8-24](https://github.com/mapbox/geojson-thumbnail/blob/662e859467c2a460445814abf57fc9f534010a89/lib/sources.js#L8-L24 "Source code on GitHub") 32 | 33 | Mapbox Streets 34 | 35 | **Parameters** 36 | 37 | - `accessToken` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** provide a valid Mapbox access token 38 | 39 | Returns **[object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** TileJSON 40 | 41 | ## mapboxSatellite 42 | 43 | [lib/sources.js:31-46](https://github.com/mapbox/geojson-thumbnail/blob/662e859467c2a460445814abf57fc9f534010a89/lib/sources.js#L31-L46 "Source code on GitHub") 44 | 45 | Mapbox Satellite 46 | 47 | **Parameters** 48 | 49 | - `accessToken` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** provide a valid Mapbox access token 50 | 51 | Returns **[object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** TileJSON 52 | 53 | ## naturalEarth 54 | 55 | [lib/sources.js:52-67](https://github.com/mapbox/geojson-thumbnail/blob/662e859467c2a460445814abf57fc9f534010a89/lib/sources.js#L52-L67 "Source code on GitHub") 56 | 57 | Natural Earth II raster tiles from 58 | 59 | Returns **[object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)** TileJSON 60 | 61 | ## default 62 | 63 | [lib/styles.js:10-10](https://github.com/mapbox/geojson-thumbnail/blob/662e859467c2a460445814abf57fc9f534010a89/lib/styles.js#L10-L10 "Source code on GitHub") 64 | 65 | A default style that visualizes geometries 66 | 67 | Returns **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Mapnik Stylesheet 68 | 69 | ## tileDistance 70 | 71 | [lib/zoom.js:10-15](https://github.com/mapbox/geojson-thumbnail/blob/662e859467c2a460445814abf57fc9f534010a89/lib/zoom.js#L10-L15 "Source code on GitHub") 72 | 73 | Calculate the diagonal distance of a tile 74 | 75 | **Parameters** 76 | 77 | - `zoomLevel` **[Number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** 78 | 79 | Returns **[Number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** distance in meters 80 | 81 | ## diagonalDistance 82 | 83 | [lib/zoom.js:26-32](https://github.com/mapbox/geojson-thumbnail/blob/662e859467c2a460445814abf57fc9f534010a89/lib/zoom.js#L26-L32 "Source code on GitHub") 84 | 85 | Calculate the diagonal distance of a bounding box 86 | 87 | * * * 88 | 89 | |\\ | 90 | | \\ | 91 | |\_\_\_\| 92 | 93 | **Parameters** 94 | 95 | - `bbox` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[Number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)>** 96 | 97 | Returns **[Number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** distance in meters 98 | 99 | ## tileZoomBboxFits 100 | 101 | [lib/zoom.js:39-50](https://github.com/mapbox/geojson-thumbnail/blob/662e859467c2a460445814abf57fc9f534010a89/lib/zoom.js#L39-L50 "Source code on GitHub") 102 | 103 | Find the max zoom level a bounding box would fit in 104 | 105 | **Parameters** 106 | 107 | - `bbox` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[Number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)>** 108 | 109 | Returns **[Number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** zoom level 110 | 111 | ## decideZoom 112 | 113 | [lib/zoom.js:58-62](https://github.com/mapbox/geojson-thumbnail/blob/662e859467c2a460445814abf57fc9f534010a89/lib/zoom.js#L58-L62 "Source code on GitHub") 114 | 115 | Given a bounding box of features try to find the best zoom level 116 | for the tiles to render to stitch image together 117 | 118 | **Parameters** 119 | 120 | - `bbox` **[Array](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)<[Number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)>** 121 | 122 | Returns **[Number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)** zoom level to request tiles at 123 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Mapbox 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # geojson-thumbnail [![Build Status](https://travis-ci.org/mapbox/geojson-thumbnail.svg?branch=master)](https://travis-ci.org/mapbox/geojson-thumbnail) [![Coverage Status](https://coveralls.io/repos/github/mapbox/geojson-thumbnail/badge.svg?branch=master)](https://coveralls.io/github/mapbox/geojson-thumbnail?branch=master) 2 | 3 | Generate thumbnails for GeoJSON features 4 | 5 | ``` 6 | npm install -g @mapbox/geojson-thumbnail 7 | npm link 8 | 9 | export MapboxAccessToken= 10 | geojson-thumbnail thumb.png 11 | ``` 12 | 13 | ![nmatsutbwh](https://user-images.githubusercontent.com/1288339/35072800-247f4dfc-fbb4-11e7-8141-b1abe76125f8.gif) 14 | 15 | ## [API](API.md#renderthumbnail) 16 | 17 | `geojson-thumbnail` exposes an API to render your own custom thumbnails of GeoJSON features. 18 | -------------------------------------------------------------------------------- /bin/geojson-thumbnail.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const program = require('commander'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const sources = require('../lib/sources'); 8 | const styles = require('../lib/styles'); 9 | const index = require('../index'); 10 | const getStdin = require('get-stdin'); 11 | 12 | program 13 | .usage(' ') 14 | .description('Render a GeoJSON thumbnail') 15 | .option('--background-satellite') 16 | .option('--no-padding') 17 | .option('--background-streets') 18 | .option('--stylesheet ') 19 | .option('--access-token ') 20 | .option('--min-zoom ') 21 | .option('--max-zoom ') 22 | .parse(process.argv); 23 | 24 | function run(inputString, output, minZoom, maxZoom, satellite, streets, stylesheetPath, accessToken, padding) { 25 | const geojson = JSON.parse(inputString); 26 | const options = { }; 27 | 28 | if (satellite) { 29 | options.background = { tilejson: sources.mapboxStellite(accessToken || process.env.MapboxAccessToken) }; 30 | } else if (streets) { 31 | options.background = { tilejson: sources.mapboxStreets(accessToken || process.env.MapboxAccessToken) }; 32 | } 33 | 34 | if (stylesheetPath === 'black') { 35 | options.stylesheet = styles.black; 36 | } else if (stylesheetPath) { 37 | options.stylesheet = fs.readFileSync(path.normalize(stylesheetPath), 'utf8'); 38 | } else { 39 | options.stylesheet = styles.default; 40 | } 41 | 42 | if (output.endsWith('.png')) { 43 | options.format = 'png'; 44 | } else if (output.endsWith('.jpg')) { 45 | options.format = 'jpeg'; 46 | } 47 | 48 | options.noPadding = !padding; 49 | 50 | if (maxZoom) { 51 | options.maxzoom = maxZoom; 52 | } 53 | if (minZoom) { 54 | options.minzoom = minZoom; 55 | } 56 | 57 | index.renderThumbnail(geojson, function onImageRendered(err, image, headers, stats) { 58 | if (err) throw err; 59 | fs.writeFile(output, image, (err) => { 60 | if (err) throw err; 61 | console.log( 62 | path.basename(output) + ',', 63 | stats.requested, 'req,', 64 | stats.rendered, 'rendered,', 65 | Math.floor(image.length / 1024), 'kb' 66 | ); 67 | }); 68 | }, options); 69 | } 70 | 71 | if (program.args.length < 2) { 72 | program.outputHelp(); 73 | } else { 74 | 75 | if (program.args[0] === '-') { 76 | getStdin().then((str) => { 77 | run(str, program.args[1], parseInt(program.minZoom), parseInt(program.maxZoom), program.backgroundSatellite, program.backgroundStreets, program.stylesheet, program.accessToken, program.padding); 78 | }); 79 | } else { 80 | run(fs.readFileSync(program.args[0]), program.args[1], parseInt(program.minZoom), parseInt(program.maxZoom), program.backgroundSatellite, program.backgroundStreets, program.stylesheet, program.accessToken, program.padding); 81 | } 82 | } 83 | 84 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const sources = require('./lib/sources'); 3 | const styles = require('./lib/styles'); 4 | const template = require('./lib/template'); 5 | const thumbnail = require('./lib/thumbnail'); 6 | const blend = require('./lib/blend'); 7 | const bestRenderParams = require('./lib/renderparams'); 8 | const TileJSON = require('@mapbox/tilejson'); 9 | const abaculus = require('@mapbox/abaculus'); 10 | 11 | function renderOverlay(geojson, options, template, callback) { 12 | const overlaySource = new thumbnail.ThumbnailSource(geojson, template, options.image, options.map); 13 | const renderParams = Object.assign(bestRenderParams(geojson, options.minzoom, options.maxzoom, !!options.noPadding), { 14 | format: options.blendFormat || 'png', 15 | tileSize: options.tileSize, 16 | getTile: overlaySource.getTile.bind(overlaySource) 17 | }); 18 | abaculus(renderParams, (err, image, headers) => { 19 | callback(err, image, headers, overlaySource.stats); 20 | }); 21 | } 22 | 23 | function renderOverlayWithBackground(geojson, options, template, callback) { 24 | const backgroundUri = { data: options.background.tilejson }; 25 | new TileJSON(backgroundUri, (err, backgroundSource) => { 26 | const overlaySource = new thumbnail.ThumbnailSource(geojson, template, options.image, options.map); 27 | const blendSource = new blend.BlendRasterSource(backgroundSource, overlaySource); 28 | const renderParams = Object.assign(bestRenderParams(geojson, options.minzoom, options.maxzoom), { 29 | format: options.blendFormat || 'png', 30 | tileSize: options.tileSize, 31 | getTile: blendSource.getTile 32 | }); 33 | abaculus(renderParams, (err, image, headers) => { 34 | callback(err, image, headers, blendSource.stats); 35 | }); 36 | }); 37 | } 38 | 39 | /** 40 | * Render a thumbnmail from a GeoJSON feature 41 | * @param {Object} geojson - GeoJSON Feature or FeatureCollection 42 | * @param {Function} callback - Callback called with rendered image once finished 43 | * @param {Object} options 44 | * @param {Object} [options.image] - Image options 45 | * @param {Object} [options.map] - Map options 46 | * @param {Object} [options.background] - Render thumbnail on a background 47 | * @param {Object} [options.background.tilejson] - TileJSON for the background layer 48 | * @param {Number} [options.minzoom] - Specify a min zoom level to render thumbnail 49 | * @param {Number} [options.maxzoom] - Specify a max zoom level to render thumbnail 50 | * @param {string} [options.format] - Format to use when blended together with the background image. https://github.com/mapbox/node-blend#options 51 | */ 52 | function renderThumbnail(geojson, callback, options) { 53 | if (!geojson) throw new Error('Cannot render thumbnail without GeoJSON passed'); 54 | // someone accidentally passed in a callback as options 55 | if (typeof options === 'function') throw new Error('Options needs to be an object not a function'); 56 | if (typeof callback !== 'function') throw new Error('Callback needs to be a function not an object'); 57 | 58 | options = Object.assign({ 59 | noPadding: false, 60 | minzoom: 0, 61 | maxzoom: 22, 62 | stylesheet: styles.default, 63 | tileSize: 256 64 | }, options); 65 | 66 | options.image = Object.assign({ 67 | tileSize: options.tileSize 68 | }, options.image); 69 | 70 | // Background source zoom always limits the possible min and maxzoom 71 | if (options.background && options.background.tilejson) { 72 | options.minzoom = Math.max(options.minzoom, options.background.tilejson.minzoom); 73 | options.maxzoom = Math.min(options.maxzoom, options.background.tilejson.maxzoom); 74 | } 75 | 76 | template.templatizeStylesheet(options.stylesheet, (err, template) => { 77 | // If no background specified we only render the overlay 78 | if (options.background && options.background.tilejson) { 79 | return renderOverlayWithBackground(geojson, options, template, callback); 80 | } else { 81 | return renderOverlay(geojson, options, template, callback); 82 | } 83 | }); 84 | } 85 | 86 | module.exports = { 87 | renderThumbnail: renderThumbnail, 88 | sources: sources, 89 | styles: styles 90 | }; 91 | -------------------------------------------------------------------------------- /lib/blend.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const blend = require('@mapbox/blend'); 3 | 4 | /** 5 | * Blend two tilelive raster sources together 6 | * @param {object} backgroundSource - tilelive source for background 7 | * @param {object} overlaySource - tilelive source for overlay 8 | * @private 9 | */ 10 | function BlendRasterSource(backgroundSource, overlaySource) { 11 | const stats = { 12 | requested: 0, 13 | rendered: 0 14 | }; 15 | 16 | function getTile(z, x, y, callback) { 17 | backgroundSource.getTile(z, x, y, function onBackgroundImage(err, imageA) { 18 | stats.requested += 1; 19 | stats.rendered += 1; 20 | if (err) return callback(err); 21 | overlaySource.getTile(z, x, y, function onOverlayImage(err, imageB) { 22 | if (err) return callback(err); 23 | stats.rendered += 1; 24 | blend([ 25 | { buffer: imageA }, 26 | { buffer: imageB } 27 | ], { }, callback); 28 | }); 29 | }); 30 | } 31 | 32 | return { 33 | stats: stats, 34 | getTile: getTile 35 | }; 36 | } 37 | 38 | exports.BlendRasterSource = BlendRasterSource; 39 | -------------------------------------------------------------------------------- /lib/renderparams.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const zoom = require('./zoom'); 3 | const bbox = require('@turf/bbox'); 4 | const sm = new (require('@mapbox/sphericalmercator'))(); 5 | 6 | module.exports = bestRenderParams; 7 | 8 | function bestRenderParams(geojson, minZoom, maxZoom, noPadding) { 9 | let optimalZoom = zoom.decideZoom(bbox(geojson)); 10 | optimalZoom = Math.max(minZoom, Math.min(maxZoom, optimalZoom)); 11 | 12 | function addPadding(extent, pad) { 13 | extent[0] -= pad; 14 | extent[1] -= pad; 15 | extent[2] += pad; 16 | extent[3] += pad; 17 | return extent; 18 | } 19 | 20 | function paddedExtent(geojson) { 21 | const extent = bbox(geojson); 22 | 23 | const topRight = sm.px([extent[2], extent[3]], optimalZoom); 24 | const bottomLeft = sm.px([extent[0], extent[1]], optimalZoom); 25 | const width = topRight[0] - bottomLeft[0]; 26 | const height = bottomLeft[1] - topRight[1]; 27 | const minSize = 200; 28 | 29 | // TODO: Padding is super hacky without any real background checking what we should do 30 | let minPad = Math.abs(sm.ll([0, 0], optimalZoom)[0] - sm.ll([10, 10], optimalZoom)[0]); 31 | 32 | if (width < minSize) { 33 | minPad = Math.abs(sm.ll([0, 0], optimalZoom)[0] - sm.ll([minSize - width, 10], optimalZoom)[0]); 34 | } 35 | if (height < minSize) { 36 | minPad = Math.abs(sm.ll([0, 0], optimalZoom)[0] - sm.ll([minSize - height, 10], optimalZoom)[0]); 37 | } 38 | 39 | const pad = Math.max( 40 | Math.abs(extent[2] - extent[0]) * 0.05, 41 | Math.abs(extent[3] - extent[1]) * 0.05, 42 | minPad, 43 | 0.001 44 | ); 45 | extent[0] -= pad; 46 | extent[1] -= pad; 47 | extent[2] += pad; 48 | extent[3] += pad; 49 | return extent; 50 | } 51 | 52 | const bounds = noPadding ? addPadding(bbox(geojson), 0.0001) : paddedExtent(geojson); 53 | return { 54 | // ensure zoom is within min and max bounds configured for thumbnail 55 | zoom: optimalZoom, 56 | scale: 1, 57 | format: 'png', 58 | bbox: bounds, 59 | limit: 36000, 60 | tileSize: 256 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /lib/sources.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Mapbox Streets https://www.mapbox.com/maps/streets/ 5 | * @param {string} accessToken - provide a valid Mapbox access token 6 | * @returns {object} TileJSON 7 | */ 8 | function mapboxStreets(accessToken) { 9 | return { 10 | autoscale: true, 11 | bounds: [-180, -85, 180, 85], 12 | format: 'png8:m=h:c=64', 13 | id: 'mapbox.streets', 14 | maxzoom: 19, 15 | minzoom: 0, 16 | name: 'Streets', 17 | tileSize: 256, 18 | scheme: 'xyz', 19 | tilejson: '2.2.0', 20 | tiles: [ 21 | `https://api.mapbox.com/styles/v1/mapbox/streets-v10/tiles/256/{z}/{x}/{y}?access_token=${accessToken}` 22 | ] 23 | }; 24 | } 25 | 26 | /** 27 | * Mapbox Satellite https://www.mapbox.com/maps/satellite/ 28 | * @param {string} accessToken - provide a valid Mapbox access token 29 | * @returns {object} TileJSON 30 | */ 31 | function mapboxSatellite(accessToken) { 32 | return { 33 | bounds: [-180, -85, 180, 85], 34 | format: 'png8:m=h:c=64', 35 | id: 'mapbox.streets-review-satellite', 36 | maxzoom: 19, 37 | minzoom: 0, 38 | name: 'Satellite', 39 | tileSize: 256, 40 | scheme: 'xyz', 41 | tilejson: '2.2.0', 42 | tiles: [ 43 | `https://api.mapbox.com/styles/v1/mapbox/satellite-v9/tiles/256/{z}/{x}/{y}?access_token=${accessToken}` 44 | ] 45 | }; 46 | } 47 | 48 | /** 49 | * Natural Earth II raster tiles from http://naturalearthtiles.lukasmartinelli.ch/ 50 | * @returns {object} TileJSON 51 | */ 52 | function naturalEarth() { 53 | return { 54 | bounds: [-180, -85, 180, 85], 55 | format: 'png8:m=h:c=64', 56 | id: 'naturalearthtiles.natural_earth_2', 57 | maxzoom: 6, 58 | minzoom: 0, 59 | name: 'Natural Earth II', 60 | tileSize: 512, 61 | scheme: 'xyz', 62 | tilejson: '2.2.0', 63 | tiles: [ 64 | 'http://naturalearthtiles.lukasmartinelli.ch/tiles/natural_earth_2.raster/{z}/{x}/{y}.png' 65 | ] 66 | }; 67 | } 68 | 69 | module.exports = { 70 | naturalEarth, 71 | mapboxStreets, 72 | mapboxSatellite 73 | }; 74 | -------------------------------------------------------------------------------- /lib/styles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | /** 7 | * A default style that visualizes geometries 8 | */ 9 | default: fs.readFileSync(path.normalize(__dirname + '/../styles/default.xml'), 'utf8'), 10 | /** 11 | * A style that draws black shapes for geoms 12 | */ 13 | black: fs.readFileSync(path.normalize(__dirname + '/../styles/black.xml'), 'utf8') 14 | }; 15 | -------------------------------------------------------------------------------- /lib/template.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const xml2js = require('xml2js'); 3 | const builder = new xml2js.Builder(); 4 | 5 | function templatizeStylesheet(stylesheet, callback) { 6 | xml2js.parseString(stylesheet, function onXmlParsed(err, result) { 7 | if (err) return callback(err); 8 | const styles = result.Map.Style.filter(function(style) { 9 | return style['$'].name.startsWith('features'); 10 | }); 11 | const refStyles = [].concat.apply([], result.Map.Layer 12 | .filter((layer) => layer['$'].name === 'features') 13 | .map((layer) => layer.StyleName)); 14 | 15 | const newTemplate = { 16 | Map: { 17 | '$': { 18 | srs: '+init=epsg:3857', 19 | 'font-directory': result.Map['$']['font-directory'] 20 | }, 21 | FontSet: result.Map.FontSet, 22 | Style: styles, 23 | Layer: [{ 24 | '$': { name: 'layer', srs: '+init=epsg:4326' }, 25 | StyleName: refStyles, 26 | Datasource: { 27 | Parameter: [ 28 | { '$': { name: 'type' }, '_': 'geojson' }, 29 | { '$': { name: 'inline' }, '_': '{{geojson}}' } 30 | ] 31 | } 32 | }] 33 | } 34 | }; 35 | 36 | // clean up optional args 37 | if (!newTemplate.Map['$']['font-directory']) delete newTemplate.Map['$']['font-directory']; 38 | if (!newTemplate.Map.FontSet) delete newTemplate.Map.FontSet; 39 | 40 | const xml = builder.buildObject(newTemplate).replace('{{geojson}}', ''); 41 | callback(null, xml); 42 | }); 43 | } 44 | 45 | exports.templatizeStylesheet = templatizeStylesheet; 46 | -------------------------------------------------------------------------------- /lib/thumbnail.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const mapnik = require('mapnik'); 3 | const sm = new (require('@mapbox/sphericalmercator'))(); 4 | const events = require('events'); 5 | 6 | if (mapnik.register_default_input_plugins) { 7 | mapnik.register_default_input_plugins(); 8 | } 9 | 10 | /** 11 | * Create a new source that returns tiles from a Mapnik xml stylesheet template 12 | * and a GeoJSON object. 13 | * @private 14 | * @param {object} GeoJSON object 15 | * @param {function} callback 16 | * @returns {undefined} 17 | */ 18 | class ThumbnailSource extends events.EventEmitter { 19 | constructor(geojson, template, imageOptions, mapOptions) { 20 | super(); 21 | this._size = imageOptions.tileSize || 256; 22 | // see image encoding options: https://github.com/mapnik/mapnik/wiki/Image-IO 23 | this._imageEncoding = imageOptions.encoding || 'png8:m=h:z=1'; 24 | this._bufferSize = 64; 25 | this._mapOptions = mapOptions || {}; 26 | this._xml = template.replace('{{geojson}}', JSON.stringify(geojson)); 27 | 28 | this.stats = { 29 | requested: 0, 30 | rendered: 0 31 | }; 32 | } 33 | 34 | /** 35 | * Gets a tile from this source. 36 | * 37 | * @param {number} z 38 | * @param {number} x 39 | * @param {number} y 40 | * @param {function} callback 41 | */ 42 | getTile(z, x, y, callback) { 43 | const encoding = this._imageEncoding; 44 | const size = this._size; 45 | const map = new mapnik.Map(size, size); 46 | map.bufferSize = this._bufferSize; 47 | 48 | try { 49 | // TODO: It is not smart or performant to create a new mapnik instance for each tile rendering 50 | this.stats.rendered += 1; 51 | map.fromString(this._xml, this._mapOptions, function onMapLoaded(err) { 52 | if (err) return callback(err); 53 | map.extent = sm.bbox(x, y, z, false, '900913'); 54 | map.render(new mapnik.Image(size, size), {}, function onImageRendered(err, image) { 55 | if (err) return callback(err); 56 | image.encode(encoding, function onImageEncoded(err, encodedImage) { 57 | callback(err, encodedImage); 58 | }); 59 | }); 60 | }); 61 | } catch (e) { 62 | callback(e); 63 | } 64 | } 65 | } 66 | 67 | exports.ThumbnailSource = ThumbnailSource; 68 | -------------------------------------------------------------------------------- /lib/zoom.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const point = require('@turf/helpers').point; 3 | const distance = require('@turf/distance'); 4 | 5 | /** 6 | * Calculate the diagonal distance of a tile 7 | * @param {Number} zoomLevel 8 | * @returns {Number} distance in meters 9 | */ 10 | function tileDistance(zoomLevel) { 11 | if (zoomLevel === 17) return 414; // the diagonal for a zoom level 17 tile 12 | if (zoomLevel > 17) throw new Error('z17 is highest zoom level'); 13 | const diagonal = 2 * tileDistance(zoomLevel + 1); 14 | return diagonal; 15 | } 16 | 17 | /** 18 | * Calculate the diagonal distance of a bounding box 19 | * _____ 20 | * |\ | 21 | * | \ | 22 | * |___\| 23 | * @param {Array} bbox 24 | * @returns {Number} distance in meters 25 | */ 26 | function diagonalDistance(bbox) { 27 | const from = point(bbox.slice(0, 2)); 28 | const to = point(bbox.slice(2, 4)); 29 | return distance(from, to, { 30 | units: 'meters' 31 | }); 32 | } 33 | 34 | /** 35 | * Find the max zoom level a bounding box would fit in 36 | * @param {Array} bbox 37 | * @returns {Number} zoom level 38 | */ 39 | function tileZoomBboxFits(bbox) { 40 | const distanceTable = [17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0].map((z) => [z, tileDistance(z)]); 41 | const d = diagonalDistance(bbox); 42 | 43 | for (let i = 0; i < distanceTable.length; i++) { 44 | const z = distanceTable[i][0]; 45 | const minDiagonal = distanceTable[i][1]; 46 | if (d <= minDiagonal) return z; 47 | } 48 | 49 | return 17; 50 | } 51 | 52 | /** 53 | * Given a bounding box of features try to find the best zoom level 54 | * for the tiles to render to stitch image together 55 | * @param {Array} bbox 56 | * @returns {Number} zoom level to request tiles at 57 | */ 58 | function decideZoom(bbox) { 59 | const z = tileZoomBboxFits(bbox); 60 | const goodReviewZoom = z + 2; 61 | return Math.min(17, goodReviewZoom); 62 | } 63 | 64 | exports.decideZoom = decideZoom; 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mapbox/geojson-thumbnail", 3 | "version": "1.2.1", 4 | "description": "Generate thumbnails for GeoJSON features", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">6.0" 8 | }, 9 | "bin": { 10 | "geojson-thumbnail": "bin/geojson-thumbnail.js" 11 | }, 12 | "scripts": { 13 | "pretest": "eslint cloudformation bin lib test index.js", 14 | "test": "nyc tape test/*.test.js | tap-spec", 15 | "coverage": "nyc --reporter lcov tape test/*.test.js && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage", 16 | "docs": "documentation build --github index.js -f md > API.md", 17 | "postinstall": "npm dedupe mapnik" 18 | }, 19 | "pre-commit": [ 20 | "pretest", 21 | "test" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/mapbox/geojson-thumbnail.git" 26 | }, 27 | "keywords": [ 28 | "geojson", 29 | "mapnik", 30 | "map", 31 | "thumbnail" 32 | ], 33 | "author": "Mapbox", 34 | "license": "BSD-3-Clause", 35 | "bugs": { 36 | "url": "https://github.com/mapbox/geojson-thumbnail/issues" 37 | }, 38 | "homepage": "https://github.com/mapbox/geojson-thumbnail#readme", 39 | "dependencies": { 40 | "@mapbox/abaculus": "^3.0.0", 41 | "@mapbox/blend": "^2.0.0", 42 | "@mapbox/sphericalmercator": "^1.0.5", 43 | "@mapbox/tilejson": "^1.1.0", 44 | "@mapbox/tilelive-overlay": "^1.0.0", 45 | "@turf/bbox": "^5.1.5", 46 | "@turf/distance": "^5.1.5", 47 | "@turf/helpers": "^6.0.0-beta.3", 48 | "commander": "^2.13.0", 49 | "get-stdin": "^6.0.0", 50 | "mapnik": "^3.7.0", 51 | "xml2js": "^0.4.19" 52 | }, 53 | "devDependencies": { 54 | "coveralls": "^3.0.0", 55 | "documentation": "^4.0.0-beta.18", 56 | "eslint": "^4.7.2", 57 | "eslint-plugin-node": "^3.0.5", 58 | "nyc": "^10.0.0", 59 | "opener": "^1.4.2", 60 | "pre-commit": "^1.2.2", 61 | "sinon": "^4.0.0", 62 | "tap-spec": "^4.1.1", 63 | "tape": "^4.6.3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /styles/black.mss: -------------------------------------------------------------------------------- 1 | @w-lines: 2; 2 | 3 | Map { 4 | background-color: #fff; //For Style guide 5 | } 6 | 7 | #features { 8 | //Style for polygon 9 | ['mapnik::geometry_type'=polygon] { 10 | line-color:black; 11 | polygon-fill: black; 12 | } 13 | 14 | //Style for lines 15 | ['mapnik::geometry_type'=linestring] { 16 | 17 | [zoom<14] { 18 | line-width: @w-lines*1.5; 19 | } 20 | [zoom>=14] { 21 | line-width: @w-lines*3; 22 | } 23 | 24 | line-width: @w-lines; 25 | line-cap:round; 26 | } 27 | 28 | ['mapnik::geometry_type'=point] { 29 | marker-width:12; 30 | marker-type:ellipse; 31 | marker-allow-overlap: true; 32 | marker-ignore-placement: true; 33 | marker-placement: point; 34 | marker-fill: black; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /styles/black.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -180,-85.0511,180,85.0511 7 | -4.2629,-2.753,16 8 | png8:m=h 9 | 22 10 | 0 11 | 12 | 13 | 14 | 35 | 37 | features 38 | 39 | -------------------------------------------------------------------------------- /styles/default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -180,-85.0511,180,85.0511 7 | -8.1388,40.313,10 8 | png8:m=h 9 | 22 10 | 0 11 | 12 | 13 | 14 | 15 | 16 | 38 | 51 | 53 | features-halo 54 | features 55 | 56 | 57 | -------------------------------------------------------------------------------- /test/fixtures/building.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "id": "way!533525339!1", 7 | "properties": { 8 | "building": "yes", 9 | "name": "timbuktu" 10 | }, 11 | "geometry": { 12 | "type": "MultiPolygon", 13 | "coordinates": [ 14 | [ 15 | [ 16 | [ 17 | -103.114977, 18 | 41.6710113 19 | ], 20 | [ 21 | -103.1143118, 22 | 41.6709992 23 | ], 24 | [ 25 | -103.1143016, 26 | 41.671315 27 | ], 28 | [ 29 | -103.1149668, 30 | 41.671327 31 | ], 32 | [ 33 | -103.114977, 34 | 41.6710113 35 | ] 36 | ] 37 | ] 38 | ] 39 | } 40 | }, 41 | { 42 | "type": "Feature", 43 | "id": "way!533525340!1", 44 | "properties": { 45 | "building": "yes", 46 | "name": "huawei road" 47 | }, 48 | "geometry": { 49 | "type": "MultiPolygon", 50 | "coordinates": [ 51 | [ 52 | [ 53 | [ 54 | -103.1128543, 55 | 41.6707276 56 | ], 57 | [ 58 | -103.1124761, 59 | 41.6707116 60 | ], 61 | [ 62 | -103.1124669, 63 | 41.6708322 64 | ], 65 | [ 66 | -103.1128451, 67 | 41.6708482 68 | ], 69 | [ 70 | -103.1128543, 71 | 41.6707276 72 | ] 73 | ] 74 | ] 75 | ] 76 | } 77 | }, 78 | { 79 | "type": "Feature", 80 | "id": "way!533525338!1", 81 | "properties": { 82 | "building": "yes" 83 | }, 84 | "geometry": { 85 | "type": "MultiPolygon", 86 | "coordinates": [ 87 | [ 88 | [ 89 | [ 90 | -103.1192395, 91 | 41.6687244 92 | ], 93 | [ 94 | -103.1188828, 95 | 41.6687244 96 | ], 97 | [ 98 | -103.1188828, 99 | 41.6690871 100 | ], 101 | [ 102 | -103.1192395, 103 | 41.6690871 104 | ], 105 | [ 106 | -103.1192395, 107 | 41.6687244 108 | ] 109 | ] 110 | ] 111 | ] 112 | } 113 | }, 114 | { 115 | "type": "Feature", 116 | "id": "way!533525337!1", 117 | "properties": { 118 | "building": "yes" 119 | }, 120 | "geometry": { 121 | "type": "MultiPolygon", 122 | "coordinates": [ 123 | [ 124 | [ 125 | [ 126 | -103.1197089, 127 | 41.6689648 128 | ], 129 | [ 130 | -103.1195265, 131 | 41.6689528 132 | ], 133 | [ 134 | -103.1195174, 135 | 41.6690302 136 | ], 137 | [ 138 | -103.1196998, 139 | 41.6690423 140 | ], 141 | [ 142 | -103.1197089, 143 | 41.6689648 144 | ] 145 | ] 146 | ] 147 | ] 148 | } 149 | }, 150 | { 151 | "type": "Feature", 152 | "id": "way!533525341!1", 153 | "properties": { 154 | "building": "yes" 155 | }, 156 | "geometry": { 157 | "type": "MultiPolygon", 158 | "coordinates": [ 159 | [ 160 | [ 161 | [ 162 | -103.1124238, 163 | 41.6712237 164 | ], 165 | [ 166 | -103.1119786, 167 | 41.6712077 168 | ], 169 | [ 170 | -103.111968, 171 | 41.6713711 172 | ], 173 | [ 174 | -103.1124133, 175 | 41.6713872 176 | ], 177 | [ 178 | -103.1124238, 179 | 41.6712237 180 | ] 181 | ] 182 | ] 183 | ] 184 | } 185 | }, 186 | { 187 | "type": "Feature", 188 | "id": "way!533525336!1", 189 | "properties": { 190 | "building": "yes" 191 | }, 192 | "geometry": { 193 | "type": "MultiPolygon", 194 | "coordinates": [ 195 | [ 196 | [ 197 | [ 198 | -103.1200855, 199 | 41.6691743 200 | ], 201 | [ 202 | -103.1198913, 203 | 41.6691692 204 | ], 205 | [ 206 | -103.1198806, 207 | 41.6693956 208 | ], 209 | [ 210 | -103.1200748, 211 | 41.6694007 212 | ], 213 | [ 214 | -103.1200855, 215 | 41.6691743 216 | ] 217 | ] 218 | ] 219 | ] 220 | } 221 | } 222 | ] 223 | } 224 | -------------------------------------------------------------------------------- /test/fixtures/peak.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "id": "node!5046488953!2", 7 | "properties": { 8 | "name": "Dallandsheia", 9 | "natural": "peak", 10 | "ssr:hovednavnetype": "Terreng", 11 | "ssr:navnetype": "Hei", 12 | "ssr:stedsnr": "810459", 13 | "ssr:url": "https://stadnamn.kartverket.no/810459" 14 | }, 15 | "geometry": { 16 | "type": "Point", 17 | "coordinates": [ 18 | 6.8248916, 19 | 58.1350856 20 | ] 21 | } 22 | }, 23 | { 24 | "type": "Feature", 25 | "id": "node!5045810511!2", 26 | "properties": { 27 | "man_made": "cairn", 28 | "name": "Buheia", 29 | "natural": "peak", 30 | "ssr:hovednavnetype": "Terreng", 31 | "ssr:navnetype": "Hei", 32 | "ssr:stedsnr": "82440", 33 | "ssr:url": "https://stadnamn.kartverket.no/82440" 34 | }, 35 | "geometry": { 36 | "type": "Point", 37 | "coordinates": [ 38 | 6.8205491, 39 | 58.1316205 40 | ] 41 | } 42 | }, 43 | { 44 | "type": "Feature", 45 | "id": "node!5046488952!2", 46 | "properties": { 47 | "alt_name": "Rundaheia", 48 | "name": "Runneheia", 49 | "natural": "peak", 50 | "ssr:hovednavnetype": "Terreng", 51 | "ssr:navnetype": "Ås", 52 | "ssr:stedsnr": "142441", 53 | "ssr:url": "https://stadnamn.kartverket.no/142441" 54 | }, 55 | "geometry": { 56 | "type": "Point", 57 | "coordinates": [ 58 | 6.8302238, 59 | 58.1333552 60 | ] 61 | } 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /test/fixtures/road.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "id": "way!69736272!2", 3 | "geometry": { 4 | "coordinates": [ 5 | [ 6 | 18.1293737562778, 7 | 49.80748803638874 8 | ], 9 | [ 10 | 18.13082317431406, 11 | 49.807613217016154 12 | ], 13 | [ 14 | 18.134375001528866, 15 | 49.80767055731934 16 | ], 17 | [ 18 | 18.1348993595291, 19 | 49.80780252108784 20 | ], 21 | [ 22 | 18.13497232171187, 23 | 49.80801775037787 24 | ], 25 | [ 26 | 18.13449371254066, 27 | 49.80887159253908 28 | ], 29 | [ 30 | 18.13450669704838, 31 | 49.809162832264775 32 | ], 33 | [ 34 | 18.134309440665305, 35 | 49.8094540702449 36 | ], 37 | [ 38 | 18.134378047732213, 39 | 49.80988693280386 40 | ], 41 | [ 42 | 18.134725502809857, 43 | 49.81046773537139 44 | ], 45 | [ 46 | 18.134936858810136, 47 | 49.811439382770345 48 | ], 49 | [ 50 | 18.137125594600093, 51 | 49.8130163230679 52 | ], 53 | [ 54 | 18.13874891015746, 55 | 49.81448788422398 56 | ], 57 | [ 58 | 18.139452421673752, 59 | 49.815101363241354 60 | ], 61 | [ 62 | 18.140655653165453, 63 | 49.81546236311087 64 | ], 65 | [ 66 | 18.141299317105194, 67 | 49.81594130822924 68 | ], 69 | [ 70 | 18.14204810148169, 71 | 49.816135371031294 72 | ], 73 | [ 74 | 18.143421997328232, 75 | 49.817375078444115 76 | ], 77 | [ 78 | 18.14381301679353, 79 | 49.81792632800472 80 | ], 81 | [ 82 | 18.14455515291121, 83 | 49.8182494281094 84 | ], 85 | [ 86 | 18.14487366075005, 87 | 49.818756883621234 88 | ], 89 | [ 90 | 18.145045, 91 | 49.818776 92 | ], 93 | [ 94 | 18.145547, 95 | 49.819263 96 | ], 97 | [ 98 | 18.145723, 99 | 49.81948 100 | ], 101 | [ 102 | 18.14588, 103 | 49.819662 104 | ], 105 | [ 106 | 18.146191, 107 | 49.819959 108 | ], 109 | [ 110 | 18.146448, 111 | 49.820151 112 | ], 113 | [ 114 | 18.146623, 115 | 49.820283 116 | ], 117 | [ 118 | 18.146807, 119 | 49.820356 120 | ], 121 | [ 122 | 18.147209, 123 | 49.820385 124 | ], 125 | [ 126 | 18.147499, 127 | 49.820394 128 | ], 129 | [ 130 | 18.147824, 131 | 49.820411 132 | ], 133 | [ 134 | 18.148266, 135 | 49.820421 136 | ], 137 | [ 138 | 18.148582, 139 | 49.820388 140 | ], 141 | [ 142 | 18.148924, 143 | 49.820361 144 | ], 145 | [ 146 | 18.14922, 147 | 49.820294 148 | ], 149 | [ 150 | 18.14954, 151 | 49.820233 152 | ], 153 | [ 154 | 18.149794, 155 | 49.8202 156 | ], 157 | [ 158 | 18.149941, 159 | 49.820286 160 | ] 161 | ], 162 | "type": "LineString" 163 | }, 164 | "type": "Feature", 165 | "properties": { 166 | "highway": "path" 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /test/fixtures/water.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "id": "way!534605487!1", 7 | "properties": { 8 | "natural": "water", 9 | "source": "NRCan-CanVec-7.0", 10 | "water": "intermittent", 11 | "osm:type": "way", 12 | "osm:id": 534605487, 13 | "osm:version": 1, 14 | "osm:changeset": 53184613, 15 | "osm:timestamp": 1508777798000, 16 | "osm:uid": 239998, 17 | "osm:user": "Sundance", 18 | "dynamosm:max_osm_timestamp": 1508777798000, 19 | "dynamosm:last_update_timestamp": 1508777936333, 20 | "iso:3166_1": "CA", 21 | "iso:3166_2": "CA-AB", 22 | "osm:user:mapping_days": 1770, 23 | "osm:user:changesetcount": 12773, 24 | "osm:user:firstedit": "2010-03-02T16:45:13.000Z", 25 | "osm:user:num_changes": 3692442 26 | }, 27 | "geometry": { 28 | "type": "MultiPolygon", 29 | "coordinates": [ 30 | [ 31 | [ 32 | [ 33 | -113.7569373, 34 | 53.4246672 35 | ], 36 | [ 37 | -113.7569051, 38 | 53.4244178 39 | ], 40 | [ 41 | -113.7566249, 42 | 53.4242191 43 | ], 44 | [ 45 | -113.7564713, 46 | 53.4236749 47 | ], 48 | [ 49 | -113.756504, 50 | 53.4232687 51 | ], 52 | [ 53 | -113.7563824, 54 | 53.4229667 55 | ], 56 | [ 57 | -113.7558791, 58 | 53.4226672 59 | ], 60 | [ 61 | -113.7554162, 62 | 53.4225415 63 | ], 64 | [ 65 | -113.7552409, 66 | 53.4225733 67 | ], 68 | [ 69 | -113.7550223, 70 | 53.4227082 71 | ], 72 | [ 73 | -113.7549276, 74 | 53.4227731 75 | ], 76 | [ 77 | -113.7547432, 78 | 53.4230067 79 | ], 80 | [ 81 | -113.7540429, 82 | 53.4235436 83 | ], 84 | [ 85 | -113.7534254, 86 | 53.4237414 87 | ], 88 | [ 89 | -113.7526315, 90 | 53.4237806 91 | ], 92 | [ 93 | -113.7524783, 94 | 53.423856 95 | ], 96 | [ 97 | -113.751633, 98 | 53.4238568 99 | ], 100 | [ 101 | -113.7509362, 102 | 53.4239927 103 | ], 104 | [ 105 | -113.7506915, 106 | 53.4241706 107 | ], 108 | [ 109 | -113.7507267, 110 | 53.4244596 111 | ], 112 | [ 113 | -113.7508987, 114 | 53.4246095 115 | ], 116 | [ 117 | -113.7511095, 118 | 53.4246476 119 | ], 120 | [ 121 | -113.7519402, 122 | 53.4246494 123 | ], 124 | [ 125 | -113.7528896, 126 | 53.4246964 127 | ], 128 | [ 129 | -113.7541585, 130 | 53.42468 131 | ], 132 | [ 133 | -113.7547164, 134 | 53.4246864 135 | ], 136 | [ 137 | -113.7552636, 138 | 53.4248398 139 | ], 140 | [ 141 | -113.7555318, 142 | 53.4249037 143 | ], 144 | [ 145 | -113.7562614, 146 | 53.4249485 147 | ], 148 | [ 149 | -113.75683, 150 | 53.4249549 151 | ], 152 | [ 153 | -113.7569373, 154 | 53.4246672 155 | ] 156 | ] 157 | ] 158 | ] 159 | } 160 | }, 161 | { 162 | "type": "Feature", 163 | "geometry": { 164 | "type": "MultiPolygon", 165 | "coordinates": [ 166 | [ 167 | [ 168 | [ 169 | -113.7506915, 170 | 53.4241706 171 | ], 172 | [ 173 | -113.7509362, 174 | 53.4239927 175 | ], 176 | [ 177 | -113.751633, 178 | 53.4238568 179 | ], 180 | [ 181 | -113.7524783, 182 | 53.423856 183 | ], 184 | [ 185 | -113.7526315, 186 | 53.4237806 187 | ], 188 | [ 189 | -113.7534254, 190 | 53.4237414 191 | ], 192 | [ 193 | -113.7540429, 194 | 53.4235436 195 | ], 196 | [ 197 | -113.7547432, 198 | 53.4230067 199 | ], 200 | [ 201 | -113.7549276, 202 | 53.4227731 203 | ], 204 | [ 205 | -113.7550223, 206 | 53.4227082 207 | ], 208 | [ 209 | -113.7552409, 210 | 53.4225733 211 | ], 212 | [ 213 | -113.7554162, 214 | 53.4225415 215 | ], 216 | [ 217 | -113.7558791, 218 | 53.4226672 219 | ], 220 | [ 221 | -113.7563824, 222 | 53.4229667 223 | ], 224 | [ 225 | -113.756504, 226 | 53.4232687 227 | ], 228 | [ 229 | -113.7564713, 230 | 53.4236749 231 | ], 232 | [ 233 | -113.7566504, 234 | 53.4243096 235 | ], 236 | [ 237 | -113.7566589, 238 | 53.4247656 239 | ], 240 | [ 241 | -113.7568192, 242 | 53.425175 243 | ], 244 | [ 245 | -113.7572393, 246 | 53.4254991 247 | ], 248 | [ 249 | -113.757816, 250 | 53.425566 251 | ], 252 | [ 253 | -113.7585971, 254 | 53.4255959 255 | ], 256 | [ 257 | -113.7587811, 258 | 53.4258008 259 | ], 260 | [ 261 | -113.7587251, 262 | 53.425973 263 | ], 264 | [ 265 | -113.7584517, 266 | 53.4262513 267 | ], 268 | [ 269 | -113.7578564, 270 | 53.4264928 271 | ], 272 | [ 273 | -113.7568653, 274 | 53.4268346 275 | ], 276 | [ 277 | -113.7561666, 278 | 53.4269042 279 | ], 280 | [ 281 | -113.7557324, 282 | 53.426678 283 | ], 284 | [ 285 | -113.7555003, 286 | 53.426253 287 | ], 288 | [ 289 | -113.7554649, 290 | 53.4254302 291 | ], 292 | [ 293 | -113.7552188, 294 | 53.4251031 295 | ], 296 | [ 297 | -113.7545636, 298 | 53.4248503 299 | ], 300 | [ 301 | -113.7528896, 302 | 53.4246964 303 | ], 304 | [ 305 | -113.7519402, 306 | 53.4246494 307 | ], 308 | [ 309 | -113.7511095, 310 | 53.4246476 311 | ], 312 | [ 313 | -113.7508987, 314 | 53.4246095 315 | ], 316 | [ 317 | -113.7507267, 318 | 53.4244596 319 | ], 320 | [ 321 | -113.7506915, 322 | 53.4241706 323 | ] 324 | ] 325 | ] 326 | ] 327 | }, 328 | "properties": { 329 | "osm:id": 98108401, 330 | "osm:type": "way", 331 | "osm:version": 2, 332 | "osm:changeset": 53184613, 333 | "osm:uid": 239998, 334 | "osm:user": "Sundance", 335 | "osm:timestamp": 1508777803000, 336 | "water": "intermittent", 337 | "source": "NRCan-CanVec-7.0", 338 | "natural": "water", 339 | "iso:3166_1": "CA", 340 | "iso:3166_2": "CA-AB", 341 | "osm:user:mapping_days": 1770, 342 | "osm:user:changesetcount": 12773, 343 | "osm:user:firstedit": "2010-03-02T16:45:13.000Z", 344 | "osm:user:num_changes": 3692442, 345 | "dynamosm:max_osm_timestamp": 1508777803000, 346 | "dynamosm:last_update_timestamp": 1508777940297 347 | }, 348 | "id": "way!98108401!2", 349 | "deleted": true 350 | }, 351 | { 352 | "type": "Feature", 353 | "id": "way!98107458!2", 354 | "properties": { 355 | "natural": "water", 356 | "source": "NRCan-CanVec-7.0", 357 | "water": "intermittent", 358 | "osm:type": "way", 359 | "osm:id": 98107458, 360 | "osm:version": 2, 361 | "osm:changeset": 53184613, 362 | "osm:timestamp": 1508777799000, 363 | "osm:uid": 239998, 364 | "osm:user": "Sundance", 365 | "dynamosm:max_osm_timestamp": 1508777799000, 366 | "dynamosm:last_update_timestamp": 1508777940709, 367 | "iso:3166_1": "CA", 368 | "iso:3166_2": "CA-AB", 369 | "osm:user:mapping_days": 1770, 370 | "osm:user:changesetcount": 12773, 371 | "osm:user:firstedit": "2010-03-02T16:45:13.000Z", 372 | "osm:user:num_changes": 3692442 373 | }, 374 | "geometry": { 375 | "type": "MultiPolygon", 376 | "coordinates": [ 377 | [ 378 | [ 379 | [ 380 | -113.7685263, 381 | 53.4258527 382 | ], 383 | [ 384 | -113.7684845, 385 | 53.4257077 386 | ], 387 | [ 388 | -113.7679568, 389 | 53.4255176 390 | ], 391 | [ 392 | -113.7665508, 393 | 53.4254201 394 | ], 395 | [ 396 | -113.766533, 397 | 53.4253852 398 | ], 399 | [ 400 | -113.7663603, 401 | 53.4253594 402 | ], 403 | [ 404 | -113.7656762, 405 | 53.4254264 406 | ], 407 | [ 408 | -113.7645603, 409 | 53.4254288 410 | ], 411 | [ 412 | -113.7641468, 413 | 53.4254943 414 | ], 415 | [ 416 | -113.7640082, 417 | 53.4257863 418 | ], 419 | [ 420 | -113.7640654, 421 | 53.4265576 422 | ], 423 | [ 424 | -113.7643169, 425 | 53.42655 426 | ], 427 | [ 428 | -113.7644511, 429 | 53.4264893 430 | ], 431 | [ 432 | -113.7645634, 433 | 53.4264386 434 | ], 435 | [ 436 | -113.7646302, 437 | 53.4263502 438 | ], 439 | [ 440 | -113.7651905, 441 | 53.4261341 442 | ], 443 | [ 444 | -113.7657863, 445 | 53.4259878 446 | ], 447 | [ 448 | -113.766842, 449 | 53.4259297 450 | ], 451 | [ 452 | -113.7682945, 453 | 53.4259615 454 | ], 455 | [ 456 | -113.7685263, 457 | 53.4258527 458 | ] 459 | ] 460 | ] 461 | ] 462 | } 463 | }, 464 | { 465 | "type": "Feature", 466 | "id": "way!534605467!1", 467 | "properties": { 468 | "source": "NRCan-CanVec-7.0", 469 | "waterway": "stream", 470 | "osm:type": "way", 471 | "osm:id": 534605467, 472 | "osm:version": 1, 473 | "osm:changeset": 53184613, 474 | "osm:timestamp": 1508777797000, 475 | "osm:uid": 239998, 476 | "osm:user": "Sundance", 477 | "dynamosm:max_osm_timestamp": 1508777797000, 478 | "dynamosm:last_update_timestamp": 1508777940770, 479 | "iso:3166_1": "CA", 480 | "iso:3166_2": "CA-AB", 481 | "osm:user:mapping_days": 1770, 482 | "osm:user:changesetcount": 12773, 483 | "osm:user:firstedit": "2010-03-02T16:45:13.000Z", 484 | "osm:user:num_changes": 3692442 485 | }, 486 | "geometry": { 487 | "type": "LineString", 488 | "coordinates": [ 489 | [ 490 | -113.7637394, 491 | 53.4266746 492 | ], 493 | [ 494 | -113.7633853, 495 | 53.4266746 496 | ] 497 | ] 498 | } 499 | }, 500 | { 501 | "type": "Feature", 502 | "id": "way!534605470!1", 503 | "properties": { 504 | "source": "NRCan-CanVec-7.0", 505 | "waterway": "stream", 506 | "osm:type": "way", 507 | "osm:id": 534605470, 508 | "osm:version": 1, 509 | "osm:changeset": 53184613, 510 | "osm:timestamp": 1508777797000, 511 | "osm:uid": 239998, 512 | "osm:user": "Sundance", 513 | "dynamosm:max_osm_timestamp": 1508777797000, 514 | "dynamosm:last_update_timestamp": 1508777940365, 515 | "iso:3166_1": "CA", 516 | "iso:3166_2": "CA-AB", 517 | "osm:user:mapping_days": 1770, 518 | "osm:user:changesetcount": 12773, 519 | "osm:user:firstedit": "2010-03-02T16:45:13.000Z", 520 | "osm:user:num_changes": 3692442 521 | }, 522 | "geometry": { 523 | "type": "LineString", 524 | "coordinates": [ 525 | [ 526 | -113.762527, 527 | 53.4266682 528 | ], 529 | [ 530 | -113.7621944, 531 | 53.426649 532 | ] 533 | ] 534 | } 535 | }, 536 | { 537 | "type": "Feature", 538 | "id": "way!534605483!1", 539 | "properties": { 540 | "layer": "-1", 541 | "source": "NRCan-CanVec-7.0", 542 | "tunnel": "culvert", 543 | "waterway": "stream", 544 | "osm:type": "way", 545 | "osm:id": 534605483, 546 | "osm:version": 1, 547 | "osm:changeset": 53184613, 548 | "osm:timestamp": 1508777798000, 549 | "osm:uid": 239998, 550 | "osm:user": "Sundance", 551 | "dynamosm:max_osm_timestamp": 1508777798000, 552 | "dynamosm:last_update_timestamp": 1508777935957, 553 | "iso:3166_1": "CA", 554 | "iso:3166_2": "CA-AB", 555 | "osm:user:mapping_days": 1770, 556 | "osm:user:changesetcount": 12773, 557 | "osm:user:firstedit": "2010-03-02T16:45:13.000Z", 558 | "osm:user:num_changes": 3692442 559 | }, 560 | "geometry": { 561 | "type": "LineString", 562 | "coordinates": [ 563 | [ 564 | -113.7640505, 565 | 53.4266618 566 | ], 567 | [ 568 | -113.7637394, 569 | 53.4266746 570 | ] 571 | ] 572 | } 573 | }, 574 | { 575 | "type": "Feature", 576 | "id": "way!98108669!3", 577 | "properties": { 578 | "source": "NRCan-CanVec-7.0", 579 | "waterway": "stream", 580 | "osm:type": "way", 581 | "osm:id": 98108669, 582 | "osm:version": 3, 583 | "osm:changeset": 53184613, 584 | "osm:timestamp": 1508777800000, 585 | "osm:uid": 239998, 586 | "osm:user": "Sundance", 587 | "dynamosm:max_osm_timestamp": 1508777800000, 588 | "dynamosm:last_update_timestamp": 1508777940627, 589 | "iso:3166_1": "CA", 590 | "iso:3166_2": "CA-AB", 591 | "osm:user:mapping_days": 1770, 592 | "osm:user:changesetcount": 12773, 593 | "osm:user:firstedit": "2010-03-02T16:45:13.000Z", 594 | "osm:user:num_changes": 3692442 595 | }, 596 | "geometry": { 597 | "type": "LineString", 598 | "coordinates": [ 599 | [ 600 | -113.7644511, 601 | 53.4264893 602 | ], 603 | [ 604 | -113.7642758, 605 | 53.4266362 606 | ], 607 | [ 608 | -113.7640505, 609 | 53.4266618 610 | ] 611 | ] 612 | } 613 | }, 614 | { 615 | "type": "Feature", 616 | "id": "way!534605476!1", 617 | "properties": { 618 | "layer": "-1", 619 | "source": "NRCan-CanVec-7.0", 620 | "tunnel": "culvert", 621 | "waterway": "stream", 622 | "osm:type": "way", 623 | "osm:id": 534605476, 624 | "osm:version": 1, 625 | "osm:changeset": 53184613, 626 | "osm:timestamp": 1508777797000, 627 | "osm:uid": 239998, 628 | "osm:user": "Sundance", 629 | "dynamosm:max_osm_timestamp": 1508777797000, 630 | "dynamosm:last_update_timestamp": 1508777940735, 631 | "iso:3166_1": "CA", 632 | "iso:3166_2": "CA-AB", 633 | "osm:user:mapping_days": 1770, 634 | "osm:user:changesetcount": 12773, 635 | "osm:user:firstedit": "2010-03-02T16:45:13.000Z", 636 | "osm:user:num_changes": 3692442 637 | }, 638 | "geometry": { 639 | "type": "LineString", 640 | "coordinates": [ 641 | [ 642 | -113.7633853, 643 | 53.4266746 644 | ], 645 | [ 646 | -113.762527, 647 | 53.4266682 648 | ] 649 | ] 650 | } 651 | }, 652 | { 653 | "type": "Feature", 654 | "id": "way!534605482!1", 655 | "properties": { 656 | "layer": "-1", 657 | "source": "NRCan-CanVec-7.0", 658 | "tunnel": "culvert", 659 | "waterway": "stream", 660 | "osm:type": "way", 661 | "osm:id": 534605482, 662 | "osm:version": 1, 663 | "osm:changeset": 53184613, 664 | "osm:timestamp": 1508777798000, 665 | "osm:uid": 239998, 666 | "osm:user": "Sundance", 667 | "dynamosm:max_osm_timestamp": 1508777798000, 668 | "dynamosm:last_update_timestamp": 1508777935570, 669 | "iso:3166_1": "CA", 670 | "iso:3166_2": "CA-AB", 671 | "osm:user:mapping_days": 1770, 672 | "osm:user:changesetcount": 12773, 673 | "osm:user:firstedit": "2010-03-02T16:45:13.000Z", 674 | "osm:user:num_changes": 3692442 675 | }, 676 | "geometry": { 677 | "type": "LineString", 678 | "coordinates": [ 679 | [ 680 | -113.7621944, 681 | 53.426649 682 | ], 683 | [ 684 | -113.7618511, 685 | 53.4266618 686 | ] 687 | ] 688 | } 689 | }, 690 | { 691 | "type": "Feature", 692 | "id": "way!534605489!1", 693 | "properties": { 694 | "natural": "water", 695 | "source": "NRCan-CanVec-7.0", 696 | "water": "intermittent", 697 | "osm:type": "way", 698 | "osm:id": 534605489, 699 | "osm:version": 1, 700 | "osm:changeset": 53184613, 701 | "osm:timestamp": 1508777798000, 702 | "osm:uid": 239998, 703 | "osm:user": "Sundance", 704 | "dynamosm:max_osm_timestamp": 1508777798000, 705 | "dynamosm:last_update_timestamp": 1508777936323, 706 | "iso:3166_1": "CA", 707 | "iso:3166_2": "CA-AB", 708 | "osm:user:mapping_days": 1770, 709 | "osm:user:changesetcount": 12773, 710 | "osm:user:firstedit": "2010-03-02T16:45:13.000Z", 711 | "osm:user:num_changes": 3692442 712 | }, 713 | "geometry": { 714 | "type": "MultiPolygon", 715 | "coordinates": [ 716 | [ 717 | [ 718 | [ 719 | -113.7587811, 720 | 53.4258008 721 | ], 722 | [ 723 | -113.7585971, 724 | 53.4255959 725 | ], 726 | [ 727 | -113.757816, 728 | 53.425566 729 | ], 730 | [ 731 | -113.7572393, 732 | 53.4254991 733 | ], 734 | [ 735 | -113.7568192, 736 | 53.425175 737 | ], 738 | [ 739 | -113.7559503, 740 | 53.4252036 741 | ], 742 | [ 743 | -113.7556509, 744 | 53.425309 745 | ], 746 | [ 747 | -113.7554649, 748 | 53.4254302 749 | ], 750 | [ 751 | -113.7555003, 752 | 53.426253 753 | ], 754 | [ 755 | -113.7557324, 756 | 53.426678 757 | ], 758 | [ 759 | -113.7561666, 760 | 53.4269042 761 | ], 762 | [ 763 | -113.7568653, 764 | 53.4268346 765 | ], 766 | [ 767 | -113.7578564, 768 | 53.4264928 769 | ], 770 | [ 771 | -113.7584517, 772 | 53.4262513 773 | ], 774 | [ 775 | -113.7587251, 776 | 53.425973 777 | ], 778 | [ 779 | -113.7587811, 780 | 53.4258008 781 | ] 782 | ] 783 | ] 784 | ] 785 | } 786 | } 787 | ], 788 | "id": "a72fd24f51817ddf6364d230fe6cb677", 789 | "properties": { 790 | "day": "2017-10-23", 791 | "layer": "water" 792 | } 793 | } 794 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const tape = require('tape'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const sources = require('../lib/sources'); 6 | const index = require('../index'); 7 | 8 | 9 | function assertThumbnailRenders(fixturePath, assert, options) { 10 | const geojson = JSON.parse(fs.readFileSync(path.join(__dirname, fixturePath))); 11 | 12 | index.renderThumbnail(geojson, (err, image) => { 13 | assert.ifError(err, 'preview should not fail'); 14 | assert.true(image.length > 10 * 1024, `preview image should have reasonable image size ${image.length}`); 15 | assert.end(); 16 | }, Object.assign({ 17 | background: { tilejson: sources.naturalEarth() } 18 | }, options)); 19 | } 20 | 21 | tape('renderThumbnail water', (assert) => { 22 | assertThumbnailRenders('/fixtures/water.geojson', assert); 23 | }); 24 | 25 | tape('renderThumbnail road', (assert) => { 26 | assertThumbnailRenders('/fixtures/road.geojson', assert); 27 | }); 28 | 29 | tape('renderThumbnail building', (assert) => { 30 | assertThumbnailRenders('/fixtures/building.geojson', assert); 31 | }); 32 | 33 | tape('renderThumbnail peak', (assert) => { 34 | assertThumbnailRenders('/fixtures/peak.geojson', assert); 35 | }); 36 | 37 | tape('renderThumbnail as png with better compression', (assert) => { 38 | assertThumbnailRenders('/fixtures/peak.geojson', assert, { 39 | format: 'png' 40 | }); 41 | }); 42 | 43 | tape('renderThumbnail as jpg', (assert) => { 44 | assertThumbnailRenders('/fixtures/peak.geojson', assert, { 45 | format: 'jpeg' 46 | }); 47 | }); 48 | 49 | tape('renderThumbnail with max zoom', (assert) => { 50 | assertThumbnailRenders('/fixtures/peak.geojson', assert, { 51 | maxzoom: 4 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/zoom.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const tape = require('tape'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const bbox = require('@turf/bbox'); 6 | 7 | const zoom = require('../lib/zoom'); 8 | 9 | tape('decideZoom', (assert) => { 10 | function fixtureBbox(fixturePath) { 11 | const feature = JSON.parse(fs.readFileSync(path.join(__dirname, fixturePath))); 12 | return bbox(feature); 13 | } 14 | const roadBbox = fixtureBbox('/fixtures/road.geojson'); 15 | assert.deepEqual(zoom.decideZoom(roadBbox), 16, 'road zoom'); 16 | 17 | const waterBbox = fixtureBbox('/fixtures/water.geojson'); 18 | assert.deepEqual(zoom.decideZoom(waterBbox), 17, 'water zoom'); 19 | 20 | const peakBbox = fixtureBbox('/fixtures/peak.geojson'); 21 | assert.deepEqual(zoom.decideZoom(peakBbox), 17, 'peak zoom'); 22 | 23 | const buildingBbox = fixtureBbox('/fixtures/building.geojson'); 24 | assert.deepEqual(zoom.decideZoom(buildingBbox), 17, 'building zoom'); 25 | 26 | assert.end(); 27 | }); 28 | 29 | --------------------------------------------------------------------------------