Skip to content

Instantly share code, notes, and snippets.

@HalfdanJ
Last active January 10, 2017 01:34
Show Gist options
  • Save HalfdanJ/cb91b5c5b916b7f05fb0ce12e9fdbeb5 to your computer and use it in GitHub Desktop.
Save HalfdanJ/cb91b5c5b916b7f05fb0ce12e9fdbeb5 to your computer and use it in GitHub Desktop.
Mapzen Vector Tile Downloader

Mapzen Vector Tile Downloader

This node.js script downloads vector tiles of maps from mapzen vector tiles. The script is ideal to download large areas of highres svg data, for example for pen plotter.

To run, install node.js, and run

npm install request underscore sphericalmercator
node app
const fs = require('fs');
const request = require('request');
const _ = require('underscore');
// Lat, lng coordinates the export is centered around
// const location = [40.729337, -73.983557]
const location = [40.710591, -73.999032]
// Number of tiles to fetch in x and y direction.
const numTiles = [10, 10];
// Zoom level to fetch tiles at (high number = more details)
const zoom = 16;
// Get api key from https://mapzen.com/developers
const api_key = 'vector-tiles-LM25tq4'
const svgScale = 0.01
// Write stream setup
const stream = fs.createWriteStream('output.svg', {flags: 'w'});
stream.write(`<svg width="1000" height="1000" xmlns="http://www.w3.org/2000/svg">`)
// Functions to calculate tile number
function long2tile(lon,zoom) { return (Math.floor((lon+180)/360*Math.pow(2,zoom))); }
function lat2tile(lat,zoom) { return (Math.floor((1-Math.log(Math.tan(lat*Math.PI/180) + 1/Math.cos(lat*Math.PI/180))/Math.PI)/2 *Math.pow(2,zoom))); }
function tile2long(x,z) { return (x/Math.pow(2,z)*360-180); }
function tile2lat(y,z) { var n=Math.PI-2*Math.PI*y/Math.pow(2,z); return (180/Math.PI*Math.atan(0.5*(Math.exp(n)-Math.exp(-n)))); }
function tile(latlng) { return [lat2tile(location[0], zoom), long2tile(location[1], zoom)] }
// Calculate tiles to download
var x = tile(location)[1]-numTiles[0]/2; // tile number x
var y = tile(location)[0]-numTiles[1]/2; // tile number y
const tx = x+numTiles[0]; // target tile number x
const ty = y+numTiles[1]; // target tile number y
const bx = x; // beginning tile number x
// Coordinate conversion
const SphericalMercator = require('SphericalMercator');
const merc = new SphericalMercator({
size: 200000
});
// Origin px position
var z = 14;
var o = merc.px([tile2long(x,zoom), tile2lat(y,zoom)], z)
// Transform lat lng to px position using mercator projection
function transform(l){
var p = merc.px([l[0], l[1]], z)
return [ (p[0]-o[0]) * svgScale , (p[1]-o[1]) * svgScale ]
}
// Download tile
function download(){
const url = "https://tile.mapzen.com/mapzen/vector/v1/buildings/" + zoom + "/" + x + "/" + y + ".json?api_key="+api_key
console.log("Downloading tile",url)
request(url, (error, response, body)=> {
if (!error && response.statusCode === 200) {
const response = JSON.parse(body)
// Render the response to svg
render(response, x, y)
// Download next tile, or end
if(x < tx){
x++
download()
} else if (y<ty){
x = bx;
y++;
download()
} else {
stream.write("</svg>")
console.log("Optimized percentage: ", _totalLinesOptimized / _totalLines)
console.log("Saved: ", _totalLines - _totalLinesOptimized, " of", _totalLines,"lines")
console.log("Traveled: ", _totalTraveled, "optimized:",_totalTraveledOptimized)
}
} else {
console.log("Got an error: ", error, ", status code: ", response.statusCode)
}
})
}
var _tileNum = 0;
var _linesCache = {}
var _totalLines = 0;
var _totalLinesOptimized = 0
var _totalTraveled = 0;
var _totalTraveledOptimized = 0;
// Takes json and renders svg
function render(json, x, y){
let lines = [];
for(var i=0;i<json.features.length;i++){
var feature = json.features[i]
// console.log(feature.properties)
if(feature.properties.kind != 'building') continue;
if(feature.geometry && (feature.geometry.type == 'LineString' )){
lines.push(feature.geometry.coordinates)
}
if(feature.geometry && (feature.geometry.type == 'MultiLineString' || feature.geometry.type == 'Polygon')){
for(var u=0;u<feature.geometry.coordinates.length; u++){
lines.push(feature.geometry.coordinates[u])
}
}
}
console.log(`${_tileNum} (${x}|${y}): ${lines.length} lines`)
_totalLines += lines.length;
// Remove duplicate lines
let filterDuplicates = (_lines, otherLines)=>{
return _.filter(_lines, (line)=>{
return !_.some(otherLines, (otherLine)=> _.isEqual(line, otherLine))
})
}
lines = _.unique(lines, (line)=>{
return JSON.stringify(line)
})
if(_linesCache[x-1]){
lines = filterDuplicates(lines, _linesCache[x-1][y])
}
if(_linesCache[x] && _linesCache[x][y-1]){
lines = filterDuplicates(lines, _linesCache[x][y-1])
}
if(_linesCache[x-1] && _linesCache[x-1][y-1]){
lines = filterDuplicates(lines, _linesCache[x-1][y-1])
}
_totalLinesOptimized += lines.length;
let distance = (p1, p2) =>{
let dX = p1[0] - p2[0];
let dY = p1[1] - p2[1];
return dX*dX + dY*dY
}
// Distance optimization
let calcDistance = (lines)=>{
let _distance = 0;
let _last
_.each(lines, (line, idx)=>{
if(idx > 0){
_distance += Math.sqrt(distance(line[0], _last))
}
_last = _.last(line)
})
return _distance;
}
_totalTraveled += calcDistance(lines)
// lines = _.sortBy(lines, (line)=>{
// return -line[0][1] + line[0][0]
// })
let linesNotAdded = lines.slice();
lines = []
for(var i=0;i<linesNotAdded.length;i){
if(lines.length == 0){
lines.push(linesNotAdded.shift())
} else {
let p = _.last(_.last(lines))
let lowestDist;
let lowestIdx = -1;
_.each(linesNotAdded, (line, idx) =>{
let dist = distance(line[0], p)
if(lowestIdx == -1 || lowestDist > dist){
lowestIdx = idx;
lowestDist = dist;
}
})
lines.push(linesNotAdded[lowestIdx])
linesNotAdded.splice(lowestIdx,1)
}
}
_totalTraveledOptimized += calcDistance(lines)
console.log(`${_tileNum} (${x}|${y}) optimized: ${lines.length} lines`)
if(!_linesCache[x]) _linesCache[x] = {}
if(!_linesCache[x][y]) _linesCache[x][y] = lines.slice();
// var colors = ['rgba(255,0,0,0.2)', 'rgba(0,255,0,0.2)', 'rgba(0,0,255,0.2)']
// var color = colors[_tileNum%3]
var color = 'rgba(0,0,0,1)'
let addPolyline = (line, color='black')=>{
stream.write('<polyline fill="none" stroke="'+color+'" points="')
stream.write(_.map(line, (c)=> transform(c).join(',')).join(' '))
stream.write('"/>')
}
for(var i=0;i<lines.length;i++){
// var color = 'rgba(0,0,0,'+i/lines.length+')'
addPolyline(lines[i],'black')
// if(i>0){
// addPolyline([_.last(lines[i-1]), _.first(lines[i])], 'magenta')
// }
}
_tileNum ++;
}
// Start the downloading
download()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment