UNPKG

tilegrinder

Version:

Easily alter all vector tiles in an MBTiles file

285 lines (219 loc) 7.06 kB
### TileGrinder - grinds for you threw a MBTile by Michael Strassburger <codepoet@cpan.org> A handy library in case you ever want to apply a processor to all vector tiles in an MBTiles without having to worry about how to pull, decode, alter, encode and store them again. ### MBTiles = require '@mapbox/mbtiles' Protobuf = require 'node-protobuf' Promise = require 'bluebird' split = require 'split' zlib = require 'zlib' path = require 'path' fs = Promise.promisifyAll require 'fs' module.exports = class TileGrinder config: debug: true output: "mbtiles" maxZoom: 14 queueSize: 64 source: null target: null protobuf: null bytesBefore: 0 bytesAfter: 0 constructor: (options) -> @config[option] = options[option] for option of options @protobuf = new Protobuf fs.readFileSync __dirname+"/../proto/vector_tile.desc" grind: (source, @destination, @callback) -> @_log "[>] starting to grind #{source} into #{@destination}" @_mustExist source .then => @_loadMBTiles source .then => @_createMBTiles @destination .then => @_grind() .then => @_waitForWrites() .then => @_log "[+] grinding done!" @_log "[>] saved #{Math.round (@bytesBefore-@bytesAfter)/@bytesBefore*100}% of storage" .catch (e) -> console.error e _grind: -> new Promise (resolve, reject) => stream = @source .createZXYStream batch: @queueSize .pipe split() queueSpots = @queueSize paused = false done = false stream .on 'data', (str) => return unless str [z, x, y] = str.split /\// return if @config.maxZoom and z > @config.maxZoom queueSpots-- if queueSpots < 1 and not paused stream.pause() paused = true @_processTile z, x, y .finally => queueSpots++ if paused and queueSpots > 0 stream.resume() paused = false resolve() if done and queueSpots is @queueSize .on 'end', => @_log "[+] waiting for workers to finish..." done = true resolve() if queueSpots is @queueSize _waitForWrites: -> return unless @config.output is "mbtiles" @_log "[+] finishing database" new Promise (resolve, reject) => @target.stopWriting (err) => reject err if err resolve() _createMBTiles: (destination) -> return unless @config.output is "mbtiles" new Promise (resolve, reject) => new MBTiles destination, (err, @target) => return reject err if err @target.startWriting (err) => return reject err if err resolve() _loadMBTiles: (source) -> new Promise (resolve, reject) => new MBTiles source, (err, @source) => if err then reject err else resolve() _updateMBTilesInfo: () -> new Promise (resolve, reject) => @source.getInfo (err, info) => return reject err if err info.mtime = Date.now() @target.putInfo info, (err) => return reject err if err resolve() _processTile: (z, x, y) -> original = null @_loadTile z, x, y .then (buffer) => @_unzipIfNeeded original = buffer .then (buffer) => @_decodeTile buffer .then (tile) => @_callback tile, z, x, y .then (tile) => @_encodeTile tile .then (buffer) => @_gzip buffer .then (buffer) => @_trackStats z, x, y, original, buffer @_storeTile z, x, y, buffer _callback: (tile, z, x, y) -> @callback tile, z, x, y tile _storeTile: (z, x, y, buffer) -> switch @config.output when "mbtiles" new Promise (resolve) => @target.putTile z, x, y, buffer, -> resolve() when "files" Promise .resolve ["/#{z}", "/#{z}/#{x}"] .mapSeries (folder) => @_createFolder @destination+folder .then => fs.writeFileAsync @destination+"/#{z}/#{x}-#{y}.pbf", buffer _createFolder: (name) -> fs .mkdirAsync path.resolve name .then -> true .catch (e) -> e.code is "EEXIST" _mustExist: (name) -> fs .statAsync path.resolve name .catch (e) -> throw new Error path+" doesn't exist." _loadTile: (z, x, y) -> new Promise (resolve, reject) => @source.getTile z, x, y, (err, tile) -> return reject err if err resolve tile _unzipIfNeeded: (buffer) -> new Promise (resolve, reject) => if @_isGzipped buffer zlib.gunzip buffer, (err, data) -> return reject err if err resolve data else resolve buffer _isGzipped: (buffer) -> buffer.slice(0,2).indexOf(Buffer.from([0x1f, 0x8b])) is 0 _gzip: (buffer) -> new Promise (resolve, reject) => zlib.gzip buffer, level: 9, (err, buffer) -> return reject err if err resolve buffer _decodeTile: (buffer) -> tile = @protobuf.parse buffer, "vector_tile.Tile" for layer in tile.layers for feature in layer.features feature.geometry = @_decodeGeometry feature.geometry tile _encodeTile: (tile) -> for layer in tile.layers for feature in layer.features feature.geometry = @_encodeGeometry feature, feature.geometry @protobuf.serialize tile, "vector_tile.Tile" _decodeGeometry: (geometry) -> idx = x = y = count = command = line = 0 lines = [] while idx < geometry.length unless count raw = geometry[idx++] command = raw & 7 count = raw >> 3 count-- if command is 1 or command is 2 x += @_dezigzag geometry[idx++] y += @_dezigzag geometry[idx++] if command is 1 lines.push line if line line = [] line.push x: x, y: y else if command is 7 line.push x: line[0].x, y: line[0].y lines.push line if line lines _encodeGeometry: (feature, geometry) -> x = y = 0 encoded = [] return [] unless geometry?.length for line in geometry encoded.push @_command 1, 1 last = line.length-1 close = feature.type is "POLYGON" and line[last].x is line[0].x and line[last].y is line[0].y for point, i in line if i is 1 encoded.push @_command 2, line.length-(if close then 2 else 1) else if close and i is last encoded.push @_command 7, 1 break dx = point.x - x dy = point.y - y encoded.push @_zigzag(dx), @_zigzag(dy) x += dx y += dy encoded _command: (command, count) -> (command & 7) | (count << 3) _zigzag: (int) -> (int << 1) ^ (int >> 31) _dezigzag: (int) -> (int >> 1) ^ -(int & 1) _trackStats: (z, x, y, original, reduced) -> saved = original.length-reduced.length @bytesBefore += original.length @bytesAfter += reduced.length @_log "[>] #{Math.round saved/original.length*100}% less data in zoom #{z}, x: #{x} y: #{y}" _log: (msg...) -> console.log msg... if @config.debug