kopia lustrzana https://github.com/rastapasta/mapscii
298 wiersze
7.0 KiB
CoffeeScript
298 wiersze
7.0 KiB
CoffeeScript
###
|
|
termap - Terminal Map Viewer
|
|
by Michael Strassburger <codepoet@cpan.org>
|
|
|
|
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"
|
|
]
|