├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── fixtures ├── 9 │ └── 150 │ │ └── 194.mvt ├── 16-46886-30383.mvt ├── 9-150-194.mvt └── 9-150-194.mvt.gz ├── index.js ├── package.json ├── test.js └── vt2geojson /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "6" -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.5 2 | 3 | - Fix the version issues 4 | 5 | ## 1.1.4 6 | 7 | - Return error when request for vector tiles fails, instead of throwing it [#13](https://github.com/mapbox/vt2geojson/issues/13) 8 | 9 | ## 1.1.3 10 | 11 | - Fix GeoJSON objects returned, which included `feauture.coordinates` which is invalid according to the GeoJSON spec [#5](https://github.com/mapbox/vt2geojson/issues/5) 12 | 13 | ## 1.1.2 14 | 15 | - Add the `vt_layer` property to features when the request consists of multiple layers [#6](https://github.com/mapbox/vt2geojson/pull/6) 16 | - Add tests for `401` and `404` responses from a tile server 17 | 18 | ## 1.1.1 19 | 20 | - :bug: fix TypeError when layers didn't exist in requested vector tile [#10](https://github.com/mapbox/vt2geojson/pull/10) 21 | 22 | ## 1.1.0 23 | 24 | - Add ability to execute command on local files [#1](https://github.com/mapbox/vt2geojson/issues/1) 25 | 26 | ## 1.0.0 27 | 28 | - First 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | ISC License 3 | 4 | Copyright (c) 2017, Mapbox 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Dump vector tiles to GeoJSON from remote URLs or local system files. 2 | 3 | ## Installation 4 | 5 | ``` 6 | npm install -g @mapbox/vt2geojson 7 | ``` 8 | 9 | ## Usage 10 | 11 | Node.js 12 | 13 | ```javascript 14 | var vt2geojson = require('@mapbox/vt2geojson'); 15 | 16 | // remote file 17 | vt2geojson({ 18 | uri: 'http://api.example.com/9/150/194.mvt', 19 | layer: 'layer_name' 20 | }, function (err, result) { 21 | if (err) throw err; 22 | console.log(result); // => GeoJSON FeatureCollection 23 | }); 24 | 25 | // local file 26 | vt2geojson({ 27 | uri: './local/file/buffer.mvt', 28 | layer: 'layer_name', 29 | z: 9, 30 | x: 150, 31 | y: 194 32 | }, function (err, result) { 33 | if (err) throw err; 34 | console.log(result); // => GeoJSON FeatureCollection 35 | }); 36 | ``` 37 | 38 | CLI 39 | 40 | ``` 41 | Usage: vt2geojson [options] URI 42 | 43 | Options: 44 | -l, --layer include only the specified layer 45 | -x tile x coordinate (normally inferred from the URI) 46 | -y tile y coordinate (normally inferred from the URI) 47 | -z tile z coordinate (normally inferred from the URI) 48 | -h, --help Show help [boolean] 49 | 50 | Examples: 51 | vt2geojson --layer state_label https://api.mapbox.com/v4/mapbox.mapbox-streets-v6/9/150/194.vector.pbf?access_token=${MAPBOX_ACCESS_TOKEN} 52 | ``` 53 | -------------------------------------------------------------------------------- /fixtures/16-46886-30383.mvt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/vt2geojson/f5e6f1f5afedb5c7f9b6ef225e25207cf8a4b5fc/fixtures/16-46886-30383.mvt -------------------------------------------------------------------------------- /fixtures/9-150-194.mvt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/vt2geojson/f5e6f1f5afedb5c7f9b6ef225e25207cf8a4b5fc/fixtures/9-150-194.mvt -------------------------------------------------------------------------------- /fixtures/9-150-194.mvt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/vt2geojson/f5e6f1f5afedb5c7f9b6ef225e25207cf8a4b5fc/fixtures/9-150-194.mvt.gz -------------------------------------------------------------------------------- /fixtures/9/150/194.mvt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mapbox/vt2geojson/f5e6f1f5afedb5c7f9b6ef225e25207cf8a4b5fc/fixtures/9/150/194.mvt -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var vt = require('vector-tile'); 4 | var request = require('request'); 5 | var Protobuf = require('pbf'); 6 | var format = require('util').format; 7 | var fs = require('fs'); 8 | var url = require('url'); 9 | var zlib = require('zlib'); 10 | 11 | module.exports = function(args, callback) { 12 | 13 | if (!args.uri) return callback(new Error('No URI found. Please provide a valid URI to your vector tile.')); 14 | 15 | // handle zxy stuffs 16 | if (args.x === undefined || args.y === undefined || args.z === undefined) { 17 | var zxy = args.uri.match(/\/(\d+)\/(\d+)\/(\d+)/); 18 | if (!zxy || zxy.length < 4) { 19 | return callback(new Error(format("Could not determine tile z, x, and y from %s; specify manually with -z -x -y ", JSON.stringify(args.uri)))); 20 | } else { 21 | args.z = zxy[1]; 22 | args.x = zxy[2]; 23 | args.y = zxy[3]; 24 | } 25 | } 26 | 27 | var parsed = url.parse(args.uri); 28 | if (parsed.protocol && (parsed.protocol === 'http:' || parsed.protocol === 'https:')) { 29 | request.get({ 30 | url: args.uri, 31 | gzip: true, 32 | encoding: null 33 | }, function (err, response, body) { 34 | if (err) { 35 | return callback(err); 36 | } 37 | if (response.statusCode === 401) { 38 | return callback(new Error('Invalid Token')); 39 | } 40 | if (response.statusCode !== 200) { 41 | return callback(new Error(format('Error retrieving data from %s. Server responded with code: %s', JSON.stringify(args.uri), response.statusCode))); 42 | } 43 | readTile(args, body, callback); 44 | }); 45 | } else { 46 | if (parsed.protocol && parsed.protocol === 'file:') { 47 | args.uri = parsed.host + parsed.pathname; 48 | } 49 | fs.lstat(args.uri, function(err, stats) { 50 | if (err) throw err; 51 | if (stats.isFile()) { 52 | fs.readFile(args.uri, function(err, data) { 53 | if (err) throw err; 54 | readTile(args, data, callback); 55 | }); 56 | } 57 | }); 58 | } 59 | }; 60 | 61 | function readTile(args, buffer, callback) { 62 | 63 | // handle zipped buffers 64 | if (buffer[0] === 0x78 && buffer[1] === 0x9C) { 65 | buffer = zlib.inflateSync(buffer); 66 | } else if (buffer[0] === 0x1F && buffer[1] === 0x8B) { 67 | buffer = zlib.gunzipSync(buffer); 68 | } 69 | 70 | var tile = new vt.VectorTile(new Protobuf(buffer)); 71 | var layers = args.layer || Object.keys(tile.layers); 72 | 73 | if (!Array.isArray(layers)) 74 | layers = [layers] 75 | 76 | var collection = {type: 'FeatureCollection', features: []}; 77 | 78 | layers.forEach(function (layerID) { 79 | var layer = tile.layers[layerID]; 80 | if (layer) { 81 | for (var i = 0; i < layer.length; i++) { 82 | var feature = layer.feature(i).toGeoJSON(args.x, args.y, args.z); 83 | if (layers.length > 1) feature.properties.vt_layer = layerID; 84 | collection.features.push(feature); 85 | } 86 | } 87 | }); 88 | 89 | callback(null, collection); 90 | } 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mapbox/vt2geojson", 3 | "description": "Dump vector tiles to GeoJSON", 4 | "version": "1.1.5", 5 | "main": "index.js", 6 | "bin": { 7 | "vt2geojson": "./vt2geojson" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/mapbox/vt2geojson.git" 12 | }, 13 | "author": "John Firebaugh ", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/mapbox/vt2geojson/issues" 17 | }, 18 | "homepage": "https://github.com/mapbox/vt2geojson", 19 | "dependencies": { 20 | "pbf": "^1.3.5", 21 | "request": "^2.64.0", 22 | "vector-tile": "git+https://github.com/mapbox/vector-tile-js.git#classify-rings", 23 | "yargs": "^3.27.0" 24 | }, 25 | "devDependencies": { 26 | "documentation": "^2.1.0-alpha2", 27 | "eslint": "^1.6.0", 28 | "eslint-config-mourner": "^1.0.1", 29 | "nock": "^2.13.0", 30 | "tap": "^2.1.1" 31 | }, 32 | "scripts": { 33 | "lint": "eslint vt2geojson *.js", 34 | "docs": "documentation --lint --github --format html --output .", 35 | "test": "npm run lint && tap --cov test.js" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tap').test; 4 | var nock = require('nock'); 5 | var fs = require('fs'); 6 | var vt2geojson = require('./'); 7 | 8 | var tile1 = nock('http://api.mapbox.com') 9 | .get('/9/150/194.mvt') 10 | .reply(200, fs.readFileSync('fixtures/9-150-194.mvt')); 11 | 12 | var tile2 = nock('http://a.tiles.mapbox.com/v4/mapbox.mapbox-streets-v7') 13 | .get('/16/46886/30383.mvt') 14 | .reply(200, fs.readFileSync('fixtures/16-46886-30383.mvt')); 15 | 16 | var invalidTokenRequest = nock('http://invalid.mapbox.com') 17 | .get('/16/46886/30383.mvt') 18 | .reply(401); 19 | 20 | var fourohfour = nock('http://404.mapbox.com') 21 | .get('/16/46886/0.mvt') 22 | .reply(404); 23 | 24 | test('fails without uri', function (t) { 25 | vt2geojson({}, function (err, result) { 26 | t.ok(err); 27 | t.equal(err.message, 'No URI found. Please provide a valid URI to your vector tile.'); 28 | t.notOk(result); 29 | t.end(); 30 | }); 31 | }); 32 | 33 | test('fails without zxy', function (t) { 34 | vt2geojson({ 35 | uri: './fixtures/9-150-194.mvt', 36 | layer: 'state_label' 37 | }, function (err, result) { 38 | t.ok(err); 39 | t.equal(err.message, 'Could not determine tile z, x, and y from "./fixtures/9-150-194.mvt"; specify manually with -z -x -y ', 'expected error message'); 40 | t.notOk(result); 41 | t.end(); 42 | }); 43 | }); 44 | 45 | test('fails with 401 response: invalid token', function (t) { 46 | vt2geojson({ 47 | uri: 'http://invalid.mapbox.com/16/46886/30383.mvt', 48 | layer: 'state_label' 49 | }, function (err, result) { 50 | t.ok(err); 51 | t.ok(/Invalid Token/.test(err.message), 'expected error message'); 52 | t.end(); 53 | }); 54 | }); 55 | 56 | test('fails 404 response', function (t) { 57 | vt2geojson({ 58 | uri: 'http://404.mapbox.com/16/46886/0.mvt', 59 | layer: 'state_label' 60 | }, function (err, result) { 61 | t.ok(err); 62 | t.ok(/Error retrieving data from/.test(err.message), 'expected error message'); 63 | t.end(); 64 | }); 65 | }); 66 | 67 | test('url', function (t) { 68 | vt2geojson({ 69 | uri: 'http://api.mapbox.com/9/150/194.mvt', 70 | layer: 'state_label' 71 | }, function (err, result) { 72 | t.ifError(err); 73 | t.deepEqual(result.type, 'FeatureCollection'); 74 | t.deepEqual(result.features[0].properties.name, 'New Jersey'); 75 | t.deepEqual(result.features[0].geometry, { 76 | type: 'Point', 77 | coordinates: [-74.38928604125977, 40.15027547340139] 78 | }); 79 | t.end(); 80 | }); 81 | }); 82 | 83 | test('undefined layer', function (t) { 84 | vt2geojson({ 85 | uri: 'http://a.tiles.mapbox.com/v4/mapbox.mapbox-streets-v7/16/46886/30383.mvt', 86 | layer: 'water' 87 | }, function (err, result) { 88 | t.ifError(err); 89 | t.deepEqual(result.type, 'FeatureCollection'); 90 | t.deepEqual(result.features.length, 0); 91 | t.end(); 92 | }); 93 | }); 94 | 95 | 96 | test('local file: relative', function (t) { 97 | vt2geojson({ 98 | uri: './fixtures/9-150-194.mvt', 99 | layer: 'state_label', 100 | z: 9, 101 | x: 150, 102 | y: 194 103 | }, function (err, result) { 104 | t.ifError(err); 105 | t.deepEqual(result.type, 'FeatureCollection'); 106 | t.deepEqual(result.features[0].properties.name, 'New Jersey'); 107 | t.deepEqual(result.features[0].geometry, { 108 | type: 'Point', 109 | coordinates: [-74.38928604125977, 40.15027547340139] 110 | }); 111 | t.end(); 112 | }); 113 | }); 114 | 115 | test('local file: absolute uri with file: protocol', function (t) { 116 | vt2geojson({ 117 | uri: 'file://./fixtures/9-150-194.mvt', 118 | layer: 'state_label', 119 | z: 9, 120 | x: 150, 121 | y: 194 122 | }, function (err, result) { 123 | t.ifError(err); 124 | t.deepEqual(result.type, 'FeatureCollection'); 125 | t.deepEqual(result.features[0].properties.name, 'New Jersey'); 126 | t.deepEqual(result.features[0].geometry, { 127 | type: 'Point', 128 | coordinates: [-74.38928604125977, 40.15027547340139] 129 | }); 130 | t.end(); 131 | }); 132 | }); 133 | 134 | test('local file with directory zxy directory structure', function (t) { 135 | vt2geojson({ 136 | uri: './fixtures/9/150/194.mvt', 137 | layer: 'state_label' 138 | }, function (err, result) { 139 | t.ifError(err); 140 | t.deepEqual(result.type, 'FeatureCollection'); 141 | t.deepEqual(result.features[0].properties.name, 'New Jersey'); 142 | t.deepEqual(result.features[0].geometry, { 143 | type: 'Point', 144 | coordinates: [-74.38928604125977, 40.15027547340139] 145 | }); 146 | t.end(); 147 | }); 148 | }); 149 | 150 | test('local file gzipped', function (t) { 151 | vt2geojson({ 152 | uri: './fixtures/9-150-194.mvt.gz', 153 | layer: 'state_label', 154 | z: 9, 155 | x: 150, 156 | y: 194 157 | }, function (err, result) { 158 | t.ifError(err); 159 | t.deepEqual(result.type, 'FeatureCollection'); 160 | t.deepEqual(result.features[0].properties.name, 'New Jersey'); 161 | t.deepEqual(result.features[0].geometry, { 162 | type: 'Point', 163 | coordinates: [-74.38928604125977, 40.15027547340139] 164 | }); 165 | t.end(); 166 | }); 167 | }); 168 | 169 | test('multiple layers adds property to preserve layer name', function (t) { 170 | vt2geojson({ 171 | uri: './fixtures/16-46886-30383.mvt', 172 | z: 16, 173 | x: 46886, 174 | y: 30383, 175 | layer: ['landuse', 'poi_label'] 176 | }, function (err, result) { 177 | t.equal(result.type, 'FeatureCollection', 'expected type'); 178 | t.equal(result.features.length, 112, 'expected number of features'); 179 | result.features.forEach(function(f) { 180 | var val = (f.properties.vt_layer === 'landuse' || f.properties.vt_layer === 'poi_label') ? true : false; 181 | t.ok(val, 'expected property value'); 182 | }); 183 | t.end(); 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /vt2geojson: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var vt2geojson = require('./'); 4 | var yargs = require('yargs'); 5 | 6 | var args = yargs 7 | .reset() 8 | .demand(1) 9 | .string('_') 10 | .usage('Usage: $0 [options] URI') 11 | 12 | .describe('l', 'include only the specified layer') 13 | .alias('l', 'layer') 14 | 15 | .describe('x', 'tile x coordinate (normally inferred from the URL)') 16 | .describe('y', 'tile y coordinate (normally inferred from the URL)') 17 | .describe('z', 'tile z coordinate (normally inferred from the URL)') 18 | 19 | .help('h') 20 | .alias('h', 'help') 21 | 22 | .example('$0 --layer state_label https://api.mapbox.com/v4/mapbox.mapbox-streets-v6/9/150/194.vector.pbf?access_token=${MAPBOX_ACCESS_TOKEN}') 23 | .example('$0 -x 150 -y 194 -z 9 --layer state_label fixtures/9-150-194.pbf') 24 | .wrap(null) 25 | .argv; 26 | 27 | args.uri = args._[0]; 28 | 29 | vt2geojson(args, function (err, geojson) { 30 | if (err) { 31 | console.error(err.message); 32 | process.exit(-1); 33 | } 34 | 35 | console.log(JSON.stringify(geojson, null, 2)); 36 | }); 37 | --------------------------------------------------------------------------------