mirror of
https://github.com/rastapasta/mapscii.git
synced 2024-11-25 09:33:48 +01:00
🔨 refactoring and becoming more classy
This commit is contained in:
parent
1effed33d9
commit
1acde4d1d3
242
src/Renderer.coffee
Normal file
242
src/Renderer.coffee
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
###
|
||||||
|
termap - Terminal Map Viewer
|
||||||
|
by Michael Strassburger <codepoet@cpan.org>
|
||||||
|
|
||||||
|
The Console Vector Tile renderer - bäm!
|
||||||
|
###
|
||||||
|
x256 = require 'x256'
|
||||||
|
Protobuf = require 'pbf'
|
||||||
|
VectorTile = require('vector-tile').VectorTile
|
||||||
|
zlib = require 'zlib'
|
||||||
|
triangulator = new (require('pnltri')).Triangulator()
|
||||||
|
|
||||||
|
Canvas = require './Canvas'
|
||||||
|
LabelBuffer = require './LabelBuffer'
|
||||||
|
Styler = require './Styler'
|
||||||
|
utils = require './utils'
|
||||||
|
|
||||||
|
module.exports = class Renderer
|
||||||
|
config:
|
||||||
|
fillPolygons: true
|
||||||
|
language: 'de'
|
||||||
|
|
||||||
|
drawOrder: ["admin", "water", "building", "road", "poi_label", "place_label", "housenum_label"]
|
||||||
|
|
||||||
|
icons:
|
||||||
|
car: "🚗"
|
||||||
|
school: "S" #{}"🏫"
|
||||||
|
marker: "⭐"
|
||||||
|
'art-gallery': "A" #"🎨"
|
||||||
|
attraction: "❕"
|
||||||
|
stadium: "🏈"
|
||||||
|
toilet: "🚽"
|
||||||
|
cafe: "☕"
|
||||||
|
laundry: "👚"
|
||||||
|
bus: "🚌"
|
||||||
|
restaurant: "R" #🍛"
|
||||||
|
lodging: "B" #🛏"
|
||||||
|
'fire-station': "🚒"
|
||||||
|
shop: "🛍"
|
||||||
|
pharmacy: "💊"
|
||||||
|
beer: "H" #"🍺"
|
||||||
|
cinema: "C" #"🎦"
|
||||||
|
|
||||||
|
layers:
|
||||||
|
housenum_label:
|
||||||
|
minZoom: 1.5
|
||||||
|
color: 8
|
||||||
|
building:
|
||||||
|
minZoom: 3.8
|
||||||
|
color: 8
|
||||||
|
|
||||||
|
place_label:
|
||||||
|
color: "yellow"
|
||||||
|
|
||||||
|
poi_label:
|
||||||
|
minZoom: 3
|
||||||
|
color: "yellow"
|
||||||
|
|
||||||
|
road:
|
||||||
|
color: 15
|
||||||
|
|
||||||
|
landuse:
|
||||||
|
color: "green"
|
||||||
|
water:
|
||||||
|
color: "blue"
|
||||||
|
admin:
|
||||||
|
color: "red"
|
||||||
|
|
||||||
|
isDrawing: false
|
||||||
|
lastDrawAt: 0
|
||||||
|
|
||||||
|
labelBuffer: null
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
@labelBuffer = new LabelBuffer()
|
||||||
|
|
||||||
|
loadStyleFile: (file) ->
|
||||||
|
@styler = new Styler file
|
||||||
|
|
||||||
|
setSize: (@width, @height) ->
|
||||||
|
@canvas = new Canvas @width, @height
|
||||||
|
|
||||||
|
_parseTile: (buffer) ->
|
||||||
|
# extract, decode and parse a given tile buffer
|
||||||
|
new VectorTile new Protobuf zlib.gunzipSync buffer
|
||||||
|
|
||||||
|
_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", "LineString", "Polygon"][feature.type]
|
||||||
|
|
||||||
|
properties = feature.properties
|
||||||
|
properties.$type = type
|
||||||
|
|
||||||
|
id: feature.id
|
||||||
|
type: type
|
||||||
|
properties: properties
|
||||||
|
points: feature.loadGeometry()
|
||||||
|
|
||||||
|
features
|
||||||
|
|
||||||
|
draw: (@view, @zoom) ->
|
||||||
|
return if @isDrawing
|
||||||
|
@isDrawing = true
|
||||||
|
@lastDrawAt = Date.now()
|
||||||
|
|
||||||
|
@labelBuffer.clear()
|
||||||
|
|
||||||
|
# TODO: better way for background color instead of setting filling FG?
|
||||||
|
# if color = @styler.styleById['background']?.paint['background-color']
|
||||||
|
# @canvas.strokeStyle = x256 utils.hex2rgb(color)...
|
||||||
|
# @canvas.fillRect 0, 0, @width, @height
|
||||||
|
# else
|
||||||
|
@canvas.clearRect 0, 0, @width, @height
|
||||||
|
|
||||||
|
@canvas.save()
|
||||||
|
@canvas.translate @view[0], @view[1]
|
||||||
|
@_drawLayers()
|
||||||
|
@canvas.restore()
|
||||||
|
|
||||||
|
@write @canvas._canvas.frame()
|
||||||
|
|
||||||
|
@isDrawing = false
|
||||||
|
|
||||||
|
write: (output) ->
|
||||||
|
process.stdout.write output
|
||||||
|
|
||||||
|
_drawLayers: ->
|
||||||
|
drawn = []
|
||||||
|
for layer in @config.drawOrder
|
||||||
|
scale = Math.pow 2, @zoom
|
||||||
|
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]
|
||||||
|
if @_drawFeature layer, feature, scale
|
||||||
|
drawn.push feature
|
||||||
|
|
||||||
|
drawn
|
||||||
|
|
||||||
|
_drawFeature: (layer, feature, scale) ->
|
||||||
|
# TODO: this is ugly :) need to be fixed @style
|
||||||
|
return false if feature.properties.class is "ferry"
|
||||||
|
feature.type = "LineString" if layer is "building" or layer is "road"
|
||||||
|
|
||||||
|
toDraw = []
|
||||||
|
for idx, points of feature.points
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
projectedPoints = for point in points
|
||||||
|
projectedPoint =
|
||||||
|
x: point.x/scale
|
||||||
|
y: point.y/scale
|
||||||
|
|
||||||
|
visible = true if not visible and @_isOnScreen projectedPoint
|
||||||
|
projectedPoint
|
||||||
|
|
||||||
|
if idx is 0 and not visible
|
||||||
|
return false
|
||||||
|
|
||||||
|
continue unless visible
|
||||||
|
toDraw.push projectedPoints
|
||||||
|
|
||||||
|
if style = @styler.getStyleFor layer, feature, 14
|
||||||
|
color = style.paint['line-color'] or style.paint['fill-color']
|
||||||
|
|
||||||
|
# TODO: zoom calculation todo for perfect styling
|
||||||
|
if color instanceof Object
|
||||||
|
color = color.stops[0][1]
|
||||||
|
|
||||||
|
@canvas.fillStyle = @canvas.strokeStyle = x256 utils.hex2rgb color
|
||||||
|
else
|
||||||
|
@canvas.strokeStyle = @canvas.fillStyle = @config.layers[layer].color
|
||||||
|
|
||||||
|
switch feature.type
|
||||||
|
when "LineString"
|
||||||
|
@_drawWithLines points for points in toDraw
|
||||||
|
true
|
||||||
|
|
||||||
|
when "Polygon"
|
||||||
|
unless @config.fillPolygons and @_drawWithTriangles toDraw
|
||||||
|
@_drawWithLines points for points in toDraw
|
||||||
|
true
|
||||||
|
|
||||||
|
when "Point"
|
||||||
|
text = feature.properties["name_"+@config.language] or
|
||||||
|
feature.properties["name"] or
|
||||||
|
feature.properties.house_num or
|
||||||
|
#@config.icons[feature.properties.maki] or
|
||||||
|
"◉"
|
||||||
|
|
||||||
|
wasDrawn = false
|
||||||
|
# TODO: check in definition if points can actually own multiple geometries
|
||||||
|
for points in toDraw
|
||||||
|
for point in points
|
||||||
|
x = point.x - text.length
|
||||||
|
#continue if x-@view[0] < 0
|
||||||
|
if @labelBuffer.writeIfPossible text, x, point.y
|
||||||
|
@canvas.fillText text, x, point.y
|
||||||
|
wasDrawn = true
|
||||||
|
|
||||||
|
wasDrawn
|
||||||
|
|
||||||
|
_drawWithTriangles: (points) ->
|
||||||
|
try
|
||||||
|
triangles = triangulator.triangulate_polygon points
|
||||||
|
catch
|
||||||
|
return false
|
||||||
|
|
||||||
|
return false unless triangles.length
|
||||||
|
|
||||||
|
# TODO: triangles are returned as vertex references to a flattened input.
|
||||||
|
# optimize it!
|
||||||
|
|
||||||
|
arr = points.reduce (a, b) -> a.concat b
|
||||||
|
for triangle in triangles
|
||||||
|
try
|
||||||
|
@canvas.fillTriangle arr[triangle[0]], arr[triangle[1]], arr[triangle[2]]
|
||||||
|
catch
|
||||||
|
return false
|
||||||
|
true
|
||||||
|
|
||||||
|
_drawWithLines: (points) ->
|
||||||
|
@canvas.beginPath()
|
||||||
|
first = points.shift()
|
||||||
|
@canvas.moveTo first.x, first.y
|
||||||
|
@canvas.lineTo point.x, point.y for point in points
|
||||||
|
@canvas.stroke()
|
||||||
|
|
||||||
|
_isOnScreen: (point) ->
|
||||||
|
point.x+@view[0]>=4 and
|
||||||
|
point.x+@view[0]<@width-4 and
|
||||||
|
point.y+@view[1]>=0 and
|
||||||
|
point.y+@view[1]<@height
|
145
src/Termap.coffee
Normal file
145
src/Termap.coffee
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
###
|
||||||
|
termap - Terminal Map Viewer
|
||||||
|
by Michael Strassburger <codepoet@cpan.org>
|
||||||
|
###
|
||||||
|
|
||||||
|
keypress = require 'keypress'
|
||||||
|
TermMouse = require 'term-mouse'
|
||||||
|
|
||||||
|
mercator = new (require('sphericalmercator'))()
|
||||||
|
|
||||||
|
Renderer = require './Renderer'
|
||||||
|
utils = require './utils'
|
||||||
|
|
||||||
|
module.exports = class Termap
|
||||||
|
config:
|
||||||
|
styleFile: __dirname+"/../styles/bright.json"
|
||||||
|
zoomStep: 0.4
|
||||||
|
|
||||||
|
width: null
|
||||||
|
height: null
|
||||||
|
canvas: null
|
||||||
|
mouse: null
|
||||||
|
|
||||||
|
mousePosition: [0, 0]
|
||||||
|
mouseDragging: false
|
||||||
|
|
||||||
|
center:
|
||||||
|
lat: 49.019855
|
||||||
|
lng: 12.096956
|
||||||
|
|
||||||
|
zoom: 2
|
||||||
|
view: [0, 0]
|
||||||
|
|
||||||
|
scale: 4
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
@_initControls()
|
||||||
|
@_initRenderer()
|
||||||
|
|
||||||
|
_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
|
||||||
|
|
||||||
|
_initRenderer: ->
|
||||||
|
@renderer = new Renderer()
|
||||||
|
@renderer.loadStyleFile @config.styleFile
|
||||||
|
|
||||||
|
process.stdout.on 'resize', =>
|
||||||
|
@_resizeRenderer()
|
||||||
|
@_draw()
|
||||||
|
|
||||||
|
@_resizeRenderer()
|
||||||
|
@zoom = Math.log(4096/@width)/Math.LN2
|
||||||
|
|
||||||
|
_resizeRenderer: (cb) ->
|
||||||
|
@width = Math.floor((process.stdout.columns-1)/2)*2*2
|
||||||
|
@height = Math.ceil(process.stdout.rows/4)*4*4
|
||||||
|
|
||||||
|
@renderer.setSize @width, @height
|
||||||
|
|
||||||
|
_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
|
||||||
|
|
||||||
|
|
||||||
|
_draw: ->
|
||||||
|
@renderer.draw @view, @zoom
|
||||||
|
@renderer.write @_getFooter()
|
||||||
|
|
||||||
|
_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: #{utils.digits @zoom, 2}"
|
||||||
|
# bbox: [#{@_getBBox().map((z) -> utils.digits(z, 2)).join(',')}]"
|
||||||
|
|
||||||
|
notify: (text) ->
|
||||||
|
return if @renderer.isDrawing
|
||||||
|
@renderer.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
|
@ -13,9 +13,6 @@ utils =
|
|||||||
angle / Math.PI * 180
|
angle / Math.PI * 180
|
||||||
|
|
||||||
hex2rgb: (color) ->
|
hex2rgb: (color) ->
|
||||||
if not color?.match
|
|
||||||
console.log color
|
|
||||||
|
|
||||||
return [255, 0, 0] unless color?.match
|
return [255, 0, 0] unless color?.match
|
||||||
|
|
||||||
unless color.match /^#[a-fA-F0-9]{3,6}$/
|
unless color.match /^#[a-fA-F0-9]{3,6}$/
|
||||||
|
@ -87,10 +87,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"hide": true,
|
||||||
"type": "line",
|
"type": "line",
|
||||||
"id": "waterway",
|
"id": "waterway",
|
||||||
"paint": {
|
"paint": {
|
||||||
"line-color": "#a0c8f0"
|
"line-color": "#303090"
|
||||||
},
|
},
|
||||||
"source-layer": "waterway",
|
"source-layer": "waterway",
|
||||||
"filter": [
|
"filter": [
|
||||||
@ -116,7 +117,7 @@
|
|||||||
"type": "line",
|
"type": "line",
|
||||||
"id": "waterway_river",
|
"id": "waterway_river",
|
||||||
"paint": {
|
"paint": {
|
||||||
"line-color": "#a0c8f0"
|
"line-color": "#303090"
|
||||||
},
|
},
|
||||||
"source-layer": "waterway",
|
"source-layer": "waterway",
|
||||||
"filter": [
|
"filter": [
|
||||||
@ -143,7 +144,7 @@
|
|||||||
"type": "fill",
|
"type": "fill",
|
||||||
"id": "water",
|
"id": "water",
|
||||||
"paint": {
|
"paint": {
|
||||||
"fill-color": "#a0c8f0"
|
"fill-color": "#303090"
|
||||||
},
|
},
|
||||||
"source-layer": "water"
|
"source-layer": "water"
|
||||||
},
|
},
|
||||||
@ -1652,6 +1653,19 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "line",
|
||||||
|
"id": "ferry_road",
|
||||||
|
"paint": {
|
||||||
|
"line-color": "#303090"
|
||||||
|
},
|
||||||
|
"source-layer": "road",
|
||||||
|
"filter": [
|
||||||
|
"==",
|
||||||
|
"class",
|
||||||
|
"ferry"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
366
termap.coffee
366
termap.coffee
@ -1,371 +1,9 @@
|
|||||||
###
|
|
||||||
termap - Terminal Map Viewer
|
|
||||||
by Michael Strassburger <codepoet@cpan.org>
|
|
||||||
###
|
|
||||||
|
|
||||||
keypress = require 'keypress'
|
|
||||||
TermMouse = require 'term-mouse'
|
|
||||||
x256 = require 'x256'
|
|
||||||
Protobuf = require 'pbf'
|
|
||||||
VectorTile = require('vector-tile').VectorTile
|
|
||||||
fs = require 'fs'
|
fs = require 'fs'
|
||||||
zlib = require 'zlib'
|
Termap = require __dirname+'/src/Termap'
|
||||||
|
|
||||||
mercator = new (require('sphericalmercator'))()
|
|
||||||
triangulator = new (require('pnltri')).Triangulator()
|
|
||||||
|
|
||||||
Canvas = require __dirname+'/src/Canvas'
|
|
||||||
LabelBuffer = require __dirname+'/src/LabelBuffer'
|
|
||||||
Styler = require __dirname+'/src/Styler'
|
|
||||||
utils = require __dirname+'/src/utils'
|
|
||||||
|
|
||||||
class Termap
|
|
||||||
config:
|
|
||||||
language: 'de'
|
|
||||||
styleFile: __dirname+"/styles/bright.json"
|
|
||||||
|
|
||||||
fillPolygons: true
|
|
||||||
zoomStep: 0.4
|
|
||||||
|
|
||||||
drawOrder: ["admin", "water", "building", "road", "poi_label", "place_label", "housenum_label"]
|
|
||||||
|
|
||||||
icons:
|
|
||||||
car: "🚗"
|
|
||||||
school: "S" #{}"🏫"
|
|
||||||
marker: "⭐"
|
|
||||||
'art-gallery': "A" #"🎨"
|
|
||||||
attraction: "❕"
|
|
||||||
stadium: "🏈"
|
|
||||||
toilet: "🚽"
|
|
||||||
cafe: "☕"
|
|
||||||
laundry: "👚"
|
|
||||||
bus: "🚌"
|
|
||||||
restaurant: "R" #🍛"
|
|
||||||
lodging: "B" #🛏"
|
|
||||||
'fire-station': "🚒"
|
|
||||||
shop: "🛍"
|
|
||||||
pharmacy: "💊"
|
|
||||||
beer: "H" #"🍺"
|
|
||||||
cinema: "C" #"🎦"
|
|
||||||
|
|
||||||
layers:
|
|
||||||
housenum_label:
|
|
||||||
minZoom: 2
|
|
||||||
color: 8
|
|
||||||
building:
|
|
||||||
minZoom: 3.8
|
|
||||||
color: 8
|
|
||||||
|
|
||||||
place_label:
|
|
||||||
color: "yellow"
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
styler: 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
|
|
||||||
|
|
||||||
unless @lastDrawAt
|
|
||||||
@zoom = Math.log(4096/@width)/Math.LN2
|
|
||||||
|
|
||||||
@labelBuffer = new LabelBuffer()
|
|
||||||
@styler = new Styler @config.styleFile
|
|
||||||
|
|
||||||
_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", "LineString", "Polygon"][feature.type]
|
|
||||||
|
|
||||||
properties = feature.properties
|
|
||||||
properties.$type = type
|
|
||||||
|
|
||||||
id: feature.id
|
|
||||||
type: type
|
|
||||||
properties: properties
|
|
||||||
points: feature.loadGeometry()
|
|
||||||
|
|
||||||
features
|
|
||||||
|
|
||||||
_draw: ->
|
|
||||||
return if @isDrawing
|
|
||||||
@isDrawing = true
|
|
||||||
@lastDrawAt = Date.now()
|
|
||||||
|
|
||||||
# if color = @styler.styleById['background']?.paint['background-color']
|
|
||||||
# @canvas.strokeStyle = x256 utils.hex2rgb(color)...
|
|
||||||
# @canvas.fillRect 0, 0, @width, @height
|
|
||||||
# else
|
|
||||||
@canvas.clearRect 0, 0, @width, @height
|
|
||||||
|
|
||||||
@canvas.save()
|
|
||||||
@canvas.translate @view[0], @view[1]
|
|
||||||
|
|
||||||
@labelBuffer.clear()
|
|
||||||
|
|
||||||
drawn = @_drawLayers()
|
|
||||||
|
|
||||||
@canvas.restore()
|
|
||||||
|
|
||||||
@_write @canvas._canvas.frame()
|
|
||||||
@_write @_getFooter()
|
|
||||||
|
|
||||||
@isDrawing = false
|
|
||||||
|
|
||||||
_drawLayers: ->
|
|
||||||
drawn = []
|
|
||||||
for layer in @config.drawOrder
|
|
||||||
scale = Math.pow 2, @zoom
|
|
||||||
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]
|
|
||||||
if @_drawFeature layer, feature, scale
|
|
||||||
drawn.push feature
|
|
||||||
|
|
||||||
drawn
|
|
||||||
|
|
||||||
_drawFeature: (layer, feature, scale) ->
|
|
||||||
# TODO: this is ugly :) need to be fixed @style
|
|
||||||
return false if layer is 'road' and feature.type is "Polygon"
|
|
||||||
feature.type = "LineString" if layer is "building"
|
|
||||||
|
|
||||||
toDraw = []
|
|
||||||
for idx, points of feature.points
|
|
||||||
visible = false
|
|
||||||
|
|
||||||
projectedPoints = for point in points
|
|
||||||
projectedPoint =
|
|
||||||
x: point.x/scale
|
|
||||||
y: point.y/scale
|
|
||||||
|
|
||||||
visible = true if not visible and @_isOnScreen projectedPoint
|
|
||||||
projectedPoint
|
|
||||||
|
|
||||||
if idx is 0 and not visible
|
|
||||||
return false
|
|
||||||
|
|
||||||
continue unless visible
|
|
||||||
toDraw.push projectedPoints
|
|
||||||
|
|
||||||
if style = @styler.getStyleFor layer, feature, 14
|
|
||||||
color = style.paint['line-color'] or style.paint['fill-color']
|
|
||||||
|
|
||||||
# TODO: zoom calculation todo for perfect styling
|
|
||||||
if color instanceof Object
|
|
||||||
color = color.stops[0][1]
|
|
||||||
|
|
||||||
@canvas.fillStyle = @canvas.strokeStyle = x256 utils.hex2rgb color
|
|
||||||
else
|
|
||||||
@canvas.strokeStyle = @canvas.fillStyle = @config.layers[layer].color
|
|
||||||
|
|
||||||
switch feature.type
|
|
||||||
when "LineString"
|
|
||||||
@_drawWithLines points for points in toDraw
|
|
||||||
true
|
|
||||||
|
|
||||||
when "Polygon"
|
|
||||||
unless @config.fillPolygons and @_drawWithTriangles toDraw
|
|
||||||
@_drawWithLines points for points in toDraw
|
|
||||||
true
|
|
||||||
|
|
||||||
when "Point"
|
|
||||||
text = feature.properties["name_"+@config.language] or
|
|
||||||
feature.properties["name"] or
|
|
||||||
feature.properties.house_num or
|
|
||||||
@config.icons[feature.properties.maki] or
|
|
||||||
"◉"
|
|
||||||
|
|
||||||
wasDrawn = false
|
|
||||||
# TODO: check in definition if points can actually own multiple geometries
|
|
||||||
for points in toDraw
|
|
||||||
for point in points
|
|
||||||
x = point.x - text.length
|
|
||||||
if @labelBuffer.writeIfPossible text, x, point.y
|
|
||||||
@canvas.fillText text, x, point.y
|
|
||||||
wasDrawn = true
|
|
||||||
|
|
||||||
wasDrawn
|
|
||||||
|
|
||||||
_drawWithTriangles: (points) ->
|
|
||||||
try
|
|
||||||
triangles = triangulator.triangulate_polygon points
|
|
||||||
catch
|
|
||||||
return false
|
|
||||||
|
|
||||||
return false unless triangles.length
|
|
||||||
|
|
||||||
# TODO: triangles are returned as vertex references to a flattened input.
|
|
||||||
# optimize it!
|
|
||||||
|
|
||||||
arr = points.reduce (a, b) -> a.concat b
|
|
||||||
for triangle in triangles
|
|
||||||
try
|
|
||||||
@canvas.fillTriangle arr[triangle[0]], arr[triangle[1]], arr[triangle[2]]
|
|
||||||
catch
|
|
||||||
return false
|
|
||||||
true
|
|
||||||
|
|
||||||
_drawWithLines: (points) ->
|
|
||||||
@canvas.beginPath()
|
|
||||||
first = points.shift()
|
|
||||||
@canvas.moveTo first.x, first.y
|
|
||||||
@canvas.lineTo point.x, point.y for point in points
|
|
||||||
@canvas.stroke()
|
|
||||||
|
|
||||||
_isOnScreen: (point) ->
|
|
||||||
point.x+@view[0]>=4 and
|
|
||||||
point.x+@view[0]<@width-4 and
|
|
||||||
point.y+@view[1]>=0 and
|
|
||||||
point.y+@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: #{utils.digits @zoom, 2}"
|
|
||||||
# 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()
|
termap = new Termap()
|
||||||
|
|
||||||
# TODO: abstracing this class, create loader class
|
# TODO: abstracing this class, create loader class
|
||||||
data = fs.readFileSync __dirname+"/tiles/regensburg.pbf.gz"
|
data = fs.readFileSync __dirname+"/tiles/regensburg.pbf.gz"
|
||||||
termap.features = termap._getFeatures termap._parseTile data
|
termap.renderer.features = termap.renderer._getFeatures termap.renderer._parseTile data
|
||||||
termap._draw()
|
termap._draw()
|
||||||
|
Loading…
Reference in New Issue
Block a user