### termap - Terminal Map Viewer by Michael Strassburger The Console Vector Tile renderer - bäm! ### tilebelt = require 'tilebelt' Promise = require 'bluebird' x256 = require 'x256' Canvas = require './Canvas' LabelBuffer = require './LabelBuffer' Styler = require './Styler' Tile = require './Tile' utils = require './utils' #simplify = require 'simplify-js' module.exports = class Renderer config: language: 'en' labelMargin: 5 tileSize: 4096 projectSize: 256 maxZoom: 14 layers: housenum_label: margin: 4 poi_label: margin: 5 cluster: true place_label: cluster: true state_label: cluster: true terminal: CLEAR: "\x1B[2J" MOVE: "\x1B[?6h" isDrawing: false lastDrawAt: 0 labelBuffer: null tileSource: null tilePadding: 64 constructor: (@output, @tileSource) -> @labelBuffer = new LabelBuffer() loadStyleFile: (file) -> @styler = new Styler file @tileSource.useStyler @styler setSize: (@width, @height) -> @canvas = new Canvas @width, @height draw: (center, zoom) -> return Promise.reject() if @isDrawing @isDrawing = true @labelBuffer.clear() @_seen = {} if color = @styler.styleById['background']?.paint['background-color'] @canvas.setBackground x256 utils.hex2rgb color @canvas.clear() Promise .resolve @_visibleTiles center, zoom .map (tile) => @_getTile tile .map (tile) => @_getTileFeatures tile .then (tiles) => @_renderTiles tiles .then => @_getFrame() .catch (e) -> console.log e .finally (frame) => @isDrawing = false @lastDrawAt = Date.now() frame _visibleTiles: (center, zoom) -> z = Math.min @config.maxZoom, Math.max 0, Math.floor zoom xyz = tilebelt.pointToTileFraction center.lon, center.lat, z tiles = [] scale = @_scaleAtZoom zoom tileSize = @config.tileSize / scale for y in [Math.floor(xyz[1])-1..Math.floor(xyz[1])+1] for x in [Math.floor(xyz[0])-1..Math.floor(xyz[0])+1] tile = x: x, y: y, z: z position = x: @width/2-(xyz[0]-tile.x)*tileSize y: @height/2-(xyz[1]-tile.y)*tileSize gridSize = Math.pow 2, z tile.x %= gridSize if tile.x < 0 tile.x = if z is 0 then 0 else tile.x+gridSize if tile.y < 0 or tile.y >= gridSize or position.x+tileSize < 0 or position.y+tileSize < 0 or position.x>@width or position.y>@height continue tiles.push xyz: tile, zoom: zoom, position: position, scale: scale tiles _getTile: (tile) -> @tileSource .getTile tile.xyz.z, tile.xyz.x, tile.xyz.y .then (data) => tile.data = data tile _getTileFeatures: (tile) -> zoom = tile.xyz.z position = tile.position scale = tile.scale box = minX: -position.x*scale minY: -position.y*scale maxX: (@width-position.x)*scale maxY: (@height-position.y)*scale features = {} for layer in @_generateDrawOrder zoom continue unless tile.data.layers?[layer] features[layer] = tile.data.layers[layer].search box tile.features = features tile _renderTiles: (tiles) -> drawn = {} for layer in @_generateDrawOrder tiles[0].xyz.z for tile in tiles continue unless tile.features[layer]?.length for feature in tile.features[layer] # continue if feature.id and drawn[feature.id] # drawn[feature.id] = true @_drawFeature tile, feature _getFrame: -> frame = "" frame += @terminal.CLEAR unless @lastDrawAt frame += @terminal.MOVE frame += @canvas.frame() frame featuresAt: (x, y) -> @labelBuffer.featuresAt x, y _scaleAtZoom: (zoom) -> baseZoom = Math.min @config.maxZoom, Math.floor Math.max 0, zoom @config.tileSize / @config.projectSize / Math.pow(2, zoom-baseZoom) _drawFeature: (tile, feature) -> if feature.style.minzoom and tile.zoom < feature.style.minzoom return false switch feature.style.type when "line" width = feature.style.paint['line-width'] width = width.stops[0][1] if width instanceof Object points = @_scaleAndReduce tile, feature, feature.points @canvas.polyline points, feature.color, width if points.length when "fill" points = (@_scaleAndReduce tile, feature, p, false for p in feature.points) @canvas.polygon points, feature.color # if points.length is 3 # @canvas._filledTriangle points[0], points[1], points[2], feature.color true when "symbol" text = feature.properties["name_"+@config.language] or feature.properties["name_en"] or feature.properties["name"] or feature.properties.house_num or "◉" points = @_scaleAndReduce tile, feature, feature.points for point in points x = point[0] - text.length margin = @config.layers[feature.layer]?.margin or @config.labelMargin if @labelBuffer.writeIfPossible text, x, point[1], feature, margin @canvas.text text, x, point[1], feature.color break else if @config.layers[feature.layer]?.cluster and @labelBuffer.writeIfPossible "X", point[0], point[1], feature, 3 @canvas.text "◉", point[0], point[1], feature.color break true _seen: {} _scaleAndReduce: (tile, feature, points, filter = true) -> lastX = null lastY = null outside = false scaled = [] # seen = {} for point in points x = Math.floor tile.position.x+(point.x/tile.scale) y = Math.floor tile.position.y+(point.y/tile.scale) if lastX is x and lastY is y continue lastY = y lastX = x # TODO: benchmark # continue if seen[idx = (y<<8)+x] # seen[idx] = true if filter if ( x < -@tilePadding or y < -@tilePadding or x > @width+@tilePadding or y > @height+@tilePadding ) continue if outside outside = true else if outside outside = null scaled.push [lastX, lastY] scaled.push [x, y] #x: x, y: y if scaled.length < 2 if feature.style.type isnt "symbol" return [] # else # scaled = ([point.x, point.y] for point in simplify scaled, 2, false) # # if filter # if scaled.length is 2 # if @_seen[ka = (scaled[0]<<8)+scaled[1]] or # @_seen[kb = (scaled[1]<<8)+scaled[0]] # return [] # # @_seen[ka] = @_seen[kb] = true scaled _generateDrawOrder: (zoom) -> if zoom < 2 [ "admin" "water" "country_label" "marine_label" ] else [ "landuse" "water" "marine_label" "building" "road" "admin" "country_label" "state_label" "water_label" "place_label" "rail_station_label" "poi_label" "road_label" "housenum_label" ]