├── .gitignore ├── .prettierrc ├── README.md ├── config.ini ├── libs ├── downloadTiles.js └── getConfig.js ├── main.js ├── package.json └── start.bat /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tiles 3 | .vscode 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "tabWidth": 4, 4 | "jsxSingleQuote": true, 5 | "printWidth": 200 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BaiduMapDownload 2 | 3 | ##### Nodejs 百度地图瓦片下载器,支持不同风格的瓦片下载,兼容windows,mac,linux。 4 | 5 | 1.执行命令 `npm i` 安装依赖。 6 | 7 | 2.修改配置文件 config.ini。 8 | 9 | > threads: 下载线程数,建议不要超过5000,过高可能被操作系统限制,导致程序退出。 10 | > 11 | > path:瓦片保存路径 12 | > 13 | > ext:文件扩展名,不建议修改,因为下载下来的都是png文件,强行修改扩展名可能导致有的程序无法读取 14 | > 15 | > minLevel:瓦片最小级别 16 | > 17 | > maxLevel:瓦片最大级别 18 | > 19 | > leftTop:地图左上角经纬度 20 | > 21 | > rightBottom:地图右下角经纬度 22 | > 23 | > customid:地图风格,可选值有常规地图样式(normal)、清新蓝风格(light)、黑夜风格(dark)、自然绿风格(grassgreen)、午夜蓝风格(midnight)、浪漫粉风格(pink)、清新蓝绿风格(bluish)、高端灰风格(grayscale) 24 | > 25 | > 边界信息和地图风格可通过[map.codezd.com](http://map.codezd.com)获取 26 | > 27 | > style:自定义样式,因为有一些特殊字符,需要加引号 28 | 29 | ```ini 30 | threads = 1000 31 | path = ./tiles 32 | minLevel = 3 33 | maxLevel = 15 34 | leftTop = 116.22952831687087,37.54514680399567 35 | rightBottom = 117.9797464385945,35.99644032407451 36 | customid = normal 37 | style = '' 38 | ``` 39 | 40 | 3.`node main` 或者执行 start.bat 开始下载。 -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | ; threads: 下载线程数 2 | ; path:瓦片保存路径 3 | ; ext:文件扩展名,不建议修改,因为下载下来的都是png文件,强行修改扩展名可能导致有的程序无法读取 4 | ; minLevel:瓦片最小级别 5 | ; maxLevel:瓦片最大级别 6 | ; leftTop:地图左上角经纬度 7 | ; rightBottom:地图右下角经纬度 8 | ; customid:地图风格,可选值有常规地图样式(normal)、清新蓝风格(light)、黑夜风格(dark)、自然绿风格(grassgreen)、午夜蓝风格(midnight)、浪漫粉风格(pink)、清新蓝绿风格(bluish)、高端灰风格(grayscale) 9 | ; style: 自定义样式,因为有一些特殊字符,需要加引号 10 | 11 | 12 | threads = 1000 13 | path = ./tiles 14 | ext = png 15 | minLevel = 3 16 | maxLevel = 18 17 | leftTop = 116.23086760055568,37.54318866104695 18 | rightBottom = 117.98189033452462,35.99643090682647 19 | customid = midnight 20 | style = 't:water|e:all|c:#101f40ff,t:land|e:all|c:#0d1e3aff,t:land|e:g|c:#0d1e3aff,t:building|e:all|c:#0d1e3aff,t:building|e:g|c:#0d1e3aff,t:building|e:g.f|c:#000000,t:building|e:g|c:#022338,t:green|e:all|c:#0a1432ff,t:arterial|e:all|c:#043144ff,t:local|e:all|c:#043144ff,t:railway|e:all|c:#043144ff,t:subway|e:all|c:#043144ff,t:highway|e:all|c:#043144ff,t:manmade|e:all|c:#0a1432ff,t:background|e:all|c:#06112cff,t:all|e:l.t.f|c:#006b89ff,t:all|e:l.t.s|c:#000000,t:all|e:l.t.f|v:on|c:#2da0c6,t:poilabel|e:l.i|v:off,t:all|e:l.i|v:off,t:boundary|e:all|c:#1e1c1c' 21 | -------------------------------------------------------------------------------- /libs/downloadTiles.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ Author: izdbrave 3 | * @ Create Time: 2019-08-01 09:12:21 4 | * @ Modified by: izdbrave 5 | * @ Modified time: 2020-04-08 13:54:35 6 | * @ Description: 下载瓦片 7 | */ 8 | 9 | const path = require('path'); 10 | 11 | const moment = require('moment'); 12 | const fs = require('fs'); 13 | const http = require('http'); 14 | 15 | const TileLnglatTransform = require('tile-lnglat-transform'); //用于经纬度转换为瓦片坐标 16 | const TileLnglatTransformBaidu = TileLnglatTransform.TileLnglatTransformBaidu; 17 | 18 | const events = require('events'); 19 | const eventEmitter = new events.EventEmitter(); 20 | 21 | const getConfig = require('./getConfig'); 22 | 23 | let totalCount = 0; //瓦片总数 24 | let downCount = 0; //已下载总数 25 | let downSize = 0; //已下载大小 26 | let errorCount = 0; //出错总数 27 | let beginTime = null; //开始时间 28 | let timer = null; //计时器 29 | let config = getConfig(); //基本配置 30 | let errLogPath = './err.log'; //错误日志 31 | let errorTilesCount = {}; //失败次数记录 32 | let httpOpiton = { 33 | timeout: 30 * 1000, 34 | }; //请求配置 35 | 36 | let tileZ = config.minLevel; //瓦片级别,瓦片Z 37 | let p1 = TileLnglatTransformBaidu.lnglatToTile(config.x1, config.y1, tileZ); //左上角 38 | let p2 = TileLnglatTransformBaidu.lnglatToTile(config.x2, config.y2, tileZ); //右下角 39 | let tileX = p1.tileX; //瓦片X 40 | let tileY = p2.tileY - 1; //瓦片Y 41 | let taskList = new Set(); //任务队列 42 | let taskCount = 0; //已添加任务数量 43 | 44 | /** 45 | * 计算时间 46 | */ 47 | function calcTime(milliseconds) { 48 | let hours = parseInt(milliseconds / 1000 / 60 / 60) 49 | .toString() 50 | .padStart(2, '0'); 51 | let minutes = (parseInt(milliseconds / 1000 / 60) % 60).toString().padStart(2, '0'); 52 | let seconds = Math.ceil(parseFloat(milliseconds / 1000) % 60) 53 | .toString() 54 | .padStart(2, '0'); 55 | return `${hours}:${minutes}:${seconds}`; 56 | } 57 | /** 58 | * 计算下载网速 59 | */ 60 | function calcNetSpeed(size) { 61 | let speed = ''; 62 | if (size < 1024) { 63 | speed = size + ' B'; 64 | } else if (size < Math.pow(1024, 2)) { 65 | speed = Math.round(size / 1024) + ' KB'; 66 | } else if (size < Math.pow(1024, 3)) { 67 | speed = Math.round((size / Math.pow(1024, 2)) * 100) / 100 + ' MB'; 68 | } else { 69 | speed = Math.round((size / Math.pow(1024, 3)) * 100) / 100 + ' GB'; 70 | } 71 | return speed + '/s'; 72 | } 73 | 74 | /** 75 | * 启动下载进程 76 | */ 77 | function download(tile) { 78 | let isError = false; 79 | let [x, y, z] = tile; 80 | let src = `http://api0.map.bdimg.com/customimage/tile?&qt=tile&x=${x}&y=${y}&z=${z}&customid=${config.customid || ''}&styles=${config.style ? encodeURIComponent(config.style) : ''}`; 81 | let errorHandler = () => { 82 | if (!isError) { 83 | isError = true; 84 | errorCallback(tile, src); 85 | } 86 | }; 87 | let req = http 88 | .get(src, httpOpiton, (res) => { 89 | let buffer = null; 90 | let contentLength = Number(res.headers['content-length']); 91 | if (res.statusCode !== 200 || isNaN(contentLength)) { 92 | errorHandler(); 93 | return; 94 | } 95 | res.on('data', (chunk) => { 96 | if (!buffer) { 97 | buffer = Buffer.from(chunk); 98 | } else { 99 | buffer = Buffer.concat([buffer, chunk]); 100 | } 101 | }) 102 | .on('end', () => { 103 | if (!isError) { 104 | if (buffer && buffer.length === contentLength && res.complete) { 105 | successCallback(tile, buffer); 106 | } else { 107 | errorHandler(); 108 | } 109 | } 110 | }) 111 | .on('aborted', (err) => { 112 | errorHandler(); 113 | }); 114 | }) 115 | .on('error', (e) => { 116 | errorHandler(); 117 | }) 118 | .on('timeout', () => { 119 | req.abort(); 120 | }); 121 | } 122 | /** 123 | * 下载成功回调 124 | */ 125 | function successCallback(tile, buffer, bb) { 126 | let dir = path.join(config.path, tile[2].toString(), tile[0].toString()); 127 | let fileName = `${tile[1]}.${config.ext || 'png'}`; 128 | if (!fs.existsSync(dir)) { 129 | fs.mkdirSync(dir, { recursive: true }); 130 | } 131 | let stream = fs.createWriteStream(path.join(dir, fileName)); 132 | stream.write(buffer); 133 | stream.end(); 134 | stream.on('close', () => { 135 | downCount++; 136 | downSize += Buffer.byteLength(buffer); 137 | eventEmitter.emit('singleTileComplete', tile); 138 | }); 139 | } 140 | /** 141 | * 下载失败回调 142 | */ 143 | function errorCallback(tile, src, k, bb) { 144 | let key = `x${tile[0]}y${tile[1]}z${tile[2]}`; 145 | if (errorTilesCount[key] === undefined) { 146 | errorTilesCount[key] = 0; 147 | } 148 | errorTilesCount[key]++; 149 | //失败重试1000万次 150 | if (errorTilesCount[key] > 10000000) { 151 | delete errorTilesCount[key]; 152 | errorCount++; 153 | console.error((key + '下载失败').red); 154 | fs.writeFileSync(errLogPath, src + '\r\n', { flag: 'a' }, function (err) {}); 155 | eventEmitter.emit('singleTileComplete', tile); 156 | } else { 157 | download(tile); 158 | } 159 | } 160 | /** 161 | * 下载回调方法 162 | */ 163 | function downloadComplete() { 164 | if (totalCount - errorCount - downCount <= 0) { 165 | let endTime = new Date(); 166 | clearInterval(timer); 167 | console.info('-------------------------------------------------------------------------------'); 168 | if (errorCount > 0) { 169 | console.info(`下载完成,共下载瓦片 ${(totalCount - errorCount).toString().green} 张,失败 ${errorCount.toString().red} 张,用时 ${calcTime(endTime - beginTime).toString().green}`.bold); 170 | console.info(`失败的瓦片请在err.log中查看`.red); 171 | } else { 172 | console.info(`下载完成,共下载瓦片 ${totalCount.toString().green} 张,用时 ${calcTime(endTime - beginTime).toString().green}`.bold); 173 | } 174 | } 175 | } 176 | /** 177 | * 显示进度信息 178 | */ 179 | function showProgressInfo() { 180 | let splitTime = 1; 181 | let preCount = 0; 182 | let preSize = 0; 183 | timer = setInterval(() => { 184 | let speed = downCount - preCount; 185 | console.info( 186 | `${moment().format('HH:mm:ss')} 速度 ${speed.toString().yellow} 张/秒 ${calcNetSpeed(downSize - preSize).toString().yellow},已完成 ${ 187 | (Math.floor(((downCount + errorCount) / totalCount) * 10000) / 100 + '%').toString().yellow 188 | },剩余 ${(totalCount - downCount - errorCount).toString().yellow} 张,${errorCount > 0 ? '失败 ' + errorCount.toString().red + ' 张,' : ''}预计还需 ${ 189 | (speed > 0 ? calcTime(((totalCount - errorCount - downCount) / speed) * 1000) : '--').toString().yellow 190 | }` 191 | ); 192 | preCount = downCount; 193 | preSize = downSize; 194 | }, splitTime * 1000); 195 | } 196 | 197 | /** 198 | * 下载瓦片 199 | */ 200 | function downloadTiles() { 201 | if (fs.existsSync(errLogPath)) { 202 | fs.unlinkSync(errLogPath); 203 | } 204 | return new Promise((resolve, reject) => { 205 | beginTime = new Date(); 206 | totalCount = calcTileCount(); 207 | console.info(`开始下载,共有瓦片 ${totalCount.toString().yellow} 张`); 208 | eventEmitter.on('singleTileComplete', (tile) => { 209 | taskList.delete(`x${tile[0]}y${tile[1]}z${tile[2]}`); 210 | if (taskList.size === 0 && taskCount === totalCount) { 211 | downloadComplete(); 212 | resolve(); 213 | } else if (taskCount < totalCount) { 214 | addTask(); 215 | } 216 | }); 217 | for (let i = 0; i < Math.min(config.threads, totalCount); i++) { 218 | addTask(); 219 | } 220 | showProgressInfo(); 221 | }); 222 | } 223 | 224 | /** 225 | * 添加任务 226 | */ 227 | function addTask() { 228 | tileY++; 229 | if (tileY > p1.tileY) { 230 | tileY = p2.tileY; 231 | tileX++; 232 | if (tileX > p2.tileX) { 233 | tileZ++; 234 | if (tileZ <= config.maxLevel) { 235 | p1 = TileLnglatTransformBaidu.lnglatToTile(config.x1, config.y1, tileZ); 236 | p2 = TileLnglatTransformBaidu.lnglatToTile(config.x2, config.y2, tileZ); 237 | 238 | tileY = p2.tileY; 239 | tileX = p1.tileX; 240 | } 241 | } 242 | } 243 | if (tileZ <= config.maxLevel) { 244 | let task = [tileX, tileY, tileZ]; 245 | taskList.add(`x${tileX}y${tileY}z${tileZ}`); 246 | download(task); 247 | taskCount++; 248 | } 249 | } 250 | /** 251 | * 计算瓦片数量 252 | */ 253 | function calcTileCount() { 254 | let count = 0; 255 | for (i = config.minLevel; i <= config.maxLevel; i++) { 256 | let p1 = TileLnglatTransformBaidu.lnglatToTile(config.x1, config.y1, i); 257 | let p2 = TileLnglatTransformBaidu.lnglatToTile(config.x2, config.y2, i); 258 | count += (Math.abs(p2.tileX - p1.tileX) + 1) * (Math.abs(p2.tileY - p1.tileY) + 1); 259 | } 260 | return count; 261 | } 262 | module.exports = downloadTiles; 263 | -------------------------------------------------------------------------------- /libs/getConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ Author: izdbrave 3 | * @ Create Time: 2019-09-03 11:42:57 4 | * @ Modified by: izdbrave 5 | * @ Modified time: 2020-03-27 14:02:59 6 | * @ Description: 读取配置文件 7 | */ 8 | 9 | const fs = require('fs'); 10 | const path = require('path'); 11 | const ini = require('ini'); 12 | const readlineSync = require('readline-sync'); 13 | /** 14 | * 读取配置文件 15 | */ 16 | function getConfig() { 17 | let configPath = path.join(process.cwd(), 'config.ini'); 18 | if (!fs.existsSync(configPath)) { 19 | console.error('找不到配置文件,请在程序目录下放置config.ini配置文件'.red); 20 | readlineSync.keyIn(); 21 | process.exit(); 22 | } 23 | let config = fs 24 | .readFileSync(configPath) 25 | .toString() 26 | .replace(/\\+/g, '\\\\'); 27 | config = ini.parse(config); 28 | config.leftTop = config.leftTop.split(','); 29 | config.rightBottom = config.rightBottom.split(','); 30 | config.leftTop.forEach(c => { 31 | c = Number(c); 32 | }); 33 | config.rightBottom.forEach(c => { 34 | c = Number(c); 35 | }); 36 | config.minLevel = Math.max(Number(config.minLevel), 3); 37 | config.maxLevel = Math.min(Number(config.maxLevel), 19); 38 | config.threads = Math.max(Number(config.threads), 1); 39 | return { 40 | x1: config.leftTop[0], 41 | y1: config.leftTop[1], 42 | x2: config.rightBottom[0], 43 | y2: config.rightBottom[1], 44 | minLevel: config.minLevel, 45 | maxLevel: config.maxLevel, 46 | style: config.style, 47 | customid: config.customid, 48 | ext: config.ext, 49 | path: config.path, 50 | threads: config.threads 51 | }; 52 | } 53 | 54 | module.exports = getConfig; 55 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ Author: izdbrave 3 | * @ Create Time: 2019-07-31 16:29:07 4 | * @ Modified by: izdbrave 5 | * @ Modified time: 2020-04-08 13:41:38 6 | * @ Description:百度地图瓦片下载 7 | */ 8 | require('colors'); 9 | const readlineSync = require('readline-sync'); 10 | const downloadTiles = require('./libs/downloadTiles'); 11 | console.log( 12 | ` 13 | ***************************************************** 14 | 百度地图瓦片下载器 v1.1 15 | Powered By 旅行者1号 16 | ***************************************************** 17 | `.trim().bold 18 | ); 19 | // 下载瓦片 20 | downloadTiles().then(() => { 21 | readlineSync.keyIn(); 22 | process.exit(); 23 | }); 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "baidumapdownload", 3 | "version": "1.0.0", 4 | "description": "百度地图瓦片下载", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/izdbrave/BaiduMapDownload.git" 12 | }, 13 | "author": "izdbrave", 14 | "license": "ISC", 15 | "dependencies": { 16 | "bagpipe": "^0.3.5", 17 | "colors": "^1.3.3", 18 | "ini": "^1.3.5", 19 | "lodash": "^4.17.15", 20 | "moment": "^2.24.0", 21 | "readline-sync": "^1.4.10", 22 | "rimraf": "^3.0.2", 23 | "tile-lnglat-transform": "^1.3.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /start.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | node main 3 | pause --------------------------------------------------------------------------------