mirror of
https://github.com/rastapasta/mapscii.git
synced 2024-11-21 23:53:08 +01:00
291 lines
7.0 KiB
CoffeeScript
291 lines
7.0 KiB
CoffeeScript
Canvas = require 'drawille-canvas-blessed-contrib'
|
|
VectorTile = require('vector-tile').VectorTile
|
|
Protobuf = require 'pbf'
|
|
keypress = require 'keypress'
|
|
fs = require 'fs'
|
|
zlib = require 'zlib'
|
|
TermMouse = require 'term-mouse'
|
|
mercator = new (require('sphericalmercator'))()
|
|
LabelBuffer = require __dirname+'/src/LabelBuffer'
|
|
|
|
utils =
|
|
deg2rad: (angle) ->
|
|
# (angle / 180) * Math.PI
|
|
angle * 0.017453292519943295
|
|
rad2deg: (angle) ->
|
|
angle / Math.PI * 180
|
|
|
|
digits: (number, digits) ->
|
|
Math.floor(number*Math.pow(10, digits))/Math.pow(10, digits)
|
|
|
|
metersPerPixel: (zoom, lat = 0) ->
|
|
utils.rad2deg(40075017*Math.cos(utils.deg2rad(lat))/Math.pow(2, zoom+8))
|
|
|
|
|
|
class Termap
|
|
config:
|
|
zoomStep: 0.5
|
|
drawOrder: ["admin", "water", "landuse", "building", "road", "poi_label", "housenum_label"]
|
|
|
|
icons:
|
|
car: "🚗"
|
|
school: "🏫"
|
|
marker: "⭐"
|
|
'art-gallery': "🎨"
|
|
attraction: "❕"
|
|
stadium: "🏈"
|
|
toilet: "🚽"
|
|
cafe: "☕"
|
|
laundry: "👚"
|
|
bus: "🚌"
|
|
restaurant: "🍛"
|
|
lodging: "🛏."
|
|
'fire-station': "🚒"
|
|
shop: "🛍"
|
|
pharmacy: "💊"
|
|
beer: "🍺"
|
|
cinema: "🎦"
|
|
|
|
layers:
|
|
housenum_label:
|
|
minZoom: 2
|
|
color: 8
|
|
building:
|
|
minZoom: 3.5
|
|
color: 8
|
|
|
|
poi_label:
|
|
minZoom: 3
|
|
color: "yellow"
|
|
|
|
road:
|
|
color: 15
|
|
|
|
landuse:
|
|
color: "green"
|
|
water:
|
|
color: "blue"
|
|
admin:
|
|
color: "red"
|
|
|
|
mouse: null
|
|
width: null
|
|
height: null
|
|
canvas: null
|
|
|
|
isDrawing: false
|
|
lastDrawAt: 0
|
|
|
|
mousePosition: [0, 0]
|
|
mouseDragging: false
|
|
|
|
center:
|
|
lat: 49.019855
|
|
lng: 12.096956
|
|
|
|
zoom: 2
|
|
view: [0, 0]
|
|
|
|
scale: 4
|
|
|
|
constructor: ->
|
|
@_initControls()
|
|
@_initCanvas()
|
|
|
|
@_onResize =>
|
|
@_initCanvas()
|
|
@_draw()
|
|
|
|
_initControls: ->
|
|
keypress process.stdin
|
|
process.stdin.setRawMode true
|
|
process.stdin.resume()
|
|
|
|
process.stdin.on 'keypress', (ch, key) => @_onKey key
|
|
|
|
@mouse = TermMouse()
|
|
@mouse.start()
|
|
|
|
@mouse.on 'click', (event) => @_onClick event
|
|
@mouse.on 'scroll', (event) => @_onMouseScroll event
|
|
@mouse.on 'move', (event) => @_onMouseMove event
|
|
|
|
_initCanvas: ->
|
|
@width = Math.floor((process.stdout.columns-1)/2)*2*2
|
|
@height = Math.ceil(process.stdout.rows/4)*4*4
|
|
@canvas = new Canvas @width, @height
|
|
|
|
@zoom = Math.log(4096/@width)/Math.LN2
|
|
|
|
_onResize: (cb) ->
|
|
process.stdout.on 'resize', cb
|
|
|
|
_onClick: (event) ->
|
|
if @mouseDragging and event.button is "left"
|
|
@view[0] -= (@mouseDragging.x-@mousePosition.x)*2
|
|
@view[1] -= (@mouseDragging.y-@mousePosition.y)*4
|
|
@_draw()
|
|
|
|
@mouseDragging = false
|
|
|
|
_onMouseScroll: (event) ->
|
|
# TODO: handle .x/y for directed zoom
|
|
@zoomBy @config.zoomStep * if event.button is "up" then 1 else -1
|
|
@_draw()
|
|
|
|
_onMouseMove: (event) ->
|
|
# only continue if x/y are valid
|
|
return unless event.x <= process.stdout.columns and event.y <= process.stdout.rows
|
|
|
|
# start dragging
|
|
if not @mouseDragging and event.button is "left"
|
|
@mouseDragging = x: event.x, y: event.y
|
|
|
|
# update internal mouse tracker
|
|
@mousePosition = x: event.x, y: event.y
|
|
|
|
_onKey: (key) ->
|
|
# check if the pressed key is configured
|
|
draw = switch key?.name
|
|
when "q"
|
|
process.exit 0
|
|
|
|
when "z" then @zoomBy @config.zoomStep
|
|
when "a" then @zoomBy -@config.zoomStep
|
|
when "left" then @view[0] += 5
|
|
when "right" then @view[0] -= 5
|
|
when "up" then @view[1]+= 5
|
|
when "down" then @view[1]-= 5
|
|
|
|
else
|
|
false
|
|
|
|
if draw
|
|
@_draw()
|
|
else
|
|
# display debug info for unhandled keys
|
|
@notify JSON.stringify key
|
|
|
|
_parseTile: (buffer) ->
|
|
# extract, decode and parse a given tile buffer
|
|
new VectorTile new Protobuf zlib.gunzipSync data
|
|
|
|
_getFeatures: (tile) ->
|
|
features = {}
|
|
for name,layer of tile.layers
|
|
continue unless @config.layers[name]
|
|
|
|
features[name] = for i in [0...layer.length]
|
|
feature = layer.feature i
|
|
|
|
type: [undefined, "point", "line", "polygon"][feature.type]
|
|
id: feature.id
|
|
properties: feature.properties
|
|
points: feature.loadGeometry()
|
|
|
|
features
|
|
|
|
_draw: ->
|
|
return if @isDrawing
|
|
@isDrawing = true
|
|
@lastDrawAt = Date.now()
|
|
|
|
@canvas.clearRect 0, 0, @width, @height
|
|
#@_write @canvas._canvas.frame()
|
|
|
|
@canvas.save()
|
|
|
|
@canvas.translate @view[0], @view[1]
|
|
|
|
scale = Math.pow 2, @zoom
|
|
|
|
drawn = []
|
|
labelBuffer = new LabelBuffer()
|
|
|
|
for layer in @config.drawOrder
|
|
continue unless @features?[layer]
|
|
|
|
if @config.layers[layer].minZoom and @zoom > @config.layers[layer].minZoom
|
|
continue
|
|
|
|
@canvas.strokeStyle = @canvas.fillStyle = @config.layers[layer].color
|
|
|
|
for feature in @features[layer]
|
|
for points in feature.points
|
|
|
|
visible = false
|
|
points = for point in points
|
|
p = [point.x/scale, point.y/scale]
|
|
if not visible and @_isOnScreen p
|
|
visible = true
|
|
p
|
|
continue unless visible
|
|
|
|
wasDrawn = false
|
|
switch feature.type
|
|
when "polygon", "line"
|
|
@canvas.beginPath()
|
|
@canvas.moveTo points.shift()...
|
|
@canvas.lineTo point... for point in points
|
|
@canvas.stroke()
|
|
wasDrawn = true
|
|
|
|
when "point"
|
|
text = feature.properties.house_num or @config.icons[feature.properties.maki] or "◉"
|
|
|
|
for point in points
|
|
if labelBuffer.writeIfPossible text, point...
|
|
@canvas.fillText text, point...
|
|
wasDrawn = true
|
|
|
|
if wasDrawn
|
|
drawn.push feature
|
|
|
|
@canvas.restore()
|
|
|
|
@_write @canvas._canvas.frame()
|
|
@_write @_getFooter()
|
|
|
|
@isDrawing = false
|
|
|
|
_isOnScreen: (point) ->
|
|
point[0]+@view[0]>=4 and
|
|
point[0]+@view[0]<@width-4 and
|
|
point[1]+@view[1]>=0 and
|
|
point[1]+@view[1]<@height
|
|
|
|
_write: (text) ->
|
|
process.stdout.write text
|
|
|
|
_getBBox: ->
|
|
[x, y] = mercator.forward [@center.lng, @center.lat]
|
|
width = @width * Math.pow(2, @zoom)
|
|
height = @height * Math.pow(2, @zoom)
|
|
|
|
mercator.inverse([x - width/2, y + width/2]).concat mercator.inverse([x + width/2, y - width/2])
|
|
|
|
_getFooter: ->
|
|
"center: [#{utils.digits @center.lat, 2}, #{utils.digits @center.lng, 2}] zoom: #{@zoom}"
|
|
# bbox: [#{@_getBBox().map((z) -> utils.digits(z, 2)).join(',')}]"
|
|
|
|
notify: (text) ->
|
|
return if @isDrawing
|
|
@_write "\r\x1B[K#{@_getFooter()} #{text}"
|
|
|
|
zoomBy: (step) ->
|
|
return unless @scale+step > 0
|
|
|
|
before = @scale
|
|
@scale += step
|
|
@zoom += step
|
|
|
|
@view[0] = @view[0]*before/@scale + if step > 0 then 8 else -8
|
|
@view[1] = @view[1]*before/@scale + if step > 0 then 8 else -8
|
|
|
|
termap = new Termap()
|
|
|
|
# TODO: abstracing this class, create loader class
|
|
data = fs.readFileSync __dirname+"/tiles/regensburg.pbf.gz"
|
|
termap.features = termap._getFeatures termap._parseTile data
|
|
termap._draw()
|