diff --git a/src/Mapscii.coffee b/src/Mapscii.coffee deleted file mode 100644 index c463dc5..0000000 --- a/src/Mapscii.coffee +++ /dev/null @@ -1,226 +0,0 @@ -### - mapscii - Terminal Map Viewer - by Michael Strassburger - - UI and central command center -### - -keypress = require 'keypress' -TermMouse = require 'term-mouse' -Promise = require 'bluebird' - -Renderer = require './Renderer' -TileSource = require './TileSource' -utils = require './utils' -config = require './config' - -module.exports = class Mapscii - width: null - height: null - canvas: null - mouse: null - - mouseDragging: false - mousePosition: - x: 0, y: 0 - - tileSource: null - renderer: null - - zoom: 0 - center: - # sf lat: 37.787946, lon: -122.407522 - # iceland lat: 64.124229, lon: -21.811552 - # rgbg - # lat: 49.019493, lon: 12.098341 - lat: 52.51298, lon: 13.42012 - - minZoom: null - - constructor: (options) -> - config[key] = val for key, val of options - - init: -> - Promise - .resolve() - .then => - unless config.headless - @_initKeyboard() - @_initMouse() - - @_initTileSource() - - .then => - @_initRenderer() - - .then => - @_draw() - .then => @notify("Welcome to MapSCII! Use your cursors to navigate, a/z to zoom, q to quit.") - - _initTileSource: -> - @tileSource = new TileSource() - @tileSource.init config.source - - _initKeyboard: -> - keypress config.input - config.input.setRawMode true if config.input.setRawMode - config.input.resume() - - config.input.on 'keypress', (ch, key) => @_onKey key - - _initMouse: -> - @mouse = TermMouse input: config.input, output: config.output - @mouse.start() - - @mouse.on 'click', (event) => @_onClick event - @mouse.on 'scroll', (event) => @_onMouseScroll event - @mouse.on 'move', (event) => @_onMouseMove event - - _initRenderer: -> - @renderer = new Renderer config.output, @tileSource - @renderer.loadStyleFile config.styleFile - - config.output.on 'resize', => - @_resizeRenderer() - @_draw() - - @_resizeRenderer() - @zoom = if config.initialZoom isnt null then config.initialZoom else @minZoom - - _resizeRenderer: (cb) -> - if config.size - @width = config.size.width - @height = config.size.height - else - @width = config.output.columns >> 1 << 2 - @height = config.output.rows * 4 - 4 - - @minZoom = 4-Math.log(4096/@width)/Math.LN2 - - @renderer.setSize @width, @height - - _updateMousePosition: (event) -> - projected = - x: (event.x-.5)*2 - y: (event.y-.5)*4 - - size = utils.tilesizeAtZoom @zoom - [dx, dy] = [projected.x-@width/2, projected.y-@height/2] - - z = utils.baseZoom @zoom - center = utils.ll2tile @center.lon, @center.lat, z - - @mousePosition = utils.normalize utils.tile2ll center.x+(dx/size), center.y+(dy/size), z - - _onClick: (event) -> - return if event.x < 0 or event.x > @width/2 or event.y < 0 or event.y > @height/4 - @_updateMousePosition event - - if @mouseDragging and event.button is "left" - @mouseDragging = false - else - @setCenter @mousePosition.lat, @mousePosition.lon - - @_draw() - - _onMouseScroll: (event) -> - @_updateMousePosition event - # TODO: handle .x/y for directed zoom - @zoomBy config.zoomStep * if event.button is "up" then 1 else -1 - @_draw() - - _onMouseMove: (event) -> - return if event.x < 0 or event.x > @width/2 or event.y < 0 or event.y > @height/4 - return if config.mouseCallback and not config.mouseCallback event - - # start dragging - if event.button is "left" - if @mouseDragging - dx = (@mouseDragging.x-event.x)*2 - dy = (@mouseDragging.y-event.y)*4 - - size = utils.tilesizeAtZoom @zoom - - newCenter = utils.tile2ll @mouseDragging.center.x+(dx/size), - @mouseDragging.center.y+(dy/size), - utils.baseZoom(@zoom) - - @setCenter newCenter.lat, newCenter.lon - - @_draw() - - else - @mouseDragging = - x: event.x, - y: event.y, - center: utils.ll2tile @center.lon, @center.lat, utils.baseZoom(@zoom) - - @_updateMousePosition event - @notify @_getFooter() - - _onKey: (key) -> - if config.keyCallback and not config.keyCallback key - return - - # check if the pressed key is configured - draw = switch key?.name - when "q" - if config.quitCallback - config.quitCallback() - else - process.exit 0 - - when "a" then @zoomBy config.zoomStep - when "z", "y" - @zoomBy -config.zoomStep - - when "left" then @moveBy 0, -8/Math.pow(2, @zoom) - when "right" then @moveBy 0, 8/Math.pow(2, @zoom) - when "up" then @moveBy 6/Math.pow(2, @zoom), 0 - when "down" then @moveBy -6/Math.pow(2, @zoom), 0 - - when "c" - config.useBraille = !config.useBraille - true - - else - null - - if draw isnt null - @_draw() - - _draw: -> - @renderer - .draw @center, @zoom - .then (frame) => - @_write frame - @notify @_getFooter() - .catch => - @notify "renderer is busy" - - _getFooter: -> - # tile = utils.ll2tile @center.lon, @center.lat, @zoom - # "tile: #{utils.digits tile.x, 3}, #{utils.digits tile.x, 3} "+ - - "center: #{utils.digits @center.lat, 3}, #{utils.digits @center.lon, 3} "+ - "zoom: #{utils.digits @zoom, 2} "+ - "mouse: #{utils.digits @mousePosition.lat, 3}, #{utils.digits @mousePosition.lon, 3} " - - notify: (text) -> - config.onUpdate() if config.onUpdate - @_write "\r\x1B[K"+text unless config.headless - - _write: (output) -> - config.output.write output - - zoomBy: (step) -> - return @zoom = @minZoom if @zoom+step < @minZoom - return @zoom = config.maxZoom if @zoom+step > config.maxZoom - - @zoom += step - - moveBy: (lat, lon) -> - @setCenter @center.lat+lat, @center.lon+lon - - setCenter: (lat, lon) -> - @center = utils.normalize lon: lon, lat: lat diff --git a/src/Mapscii.js b/src/Mapscii.js new file mode 100644 index 0000000..6d230c8 --- /dev/null +++ b/src/Mapscii.js @@ -0,0 +1,287 @@ +/* + mapscii - Terminal Map Viewer + by Michael Strassburger + + UI and central command center +*/ +'use strict'; +const keypress = require('keypress'); +const TermMouse = require('term-mouse'); + +const Renderer = require('./Renderer'); +const TileSource = require('./TileSource'); +const utils = require('./utils'); +let config = require('./config'); + +class Mapscii { + constructor(options) { + this.width = null; + this.height = null; + this.canvas = null; + this.mouse = null; + + this.mouseDragging = false; + this.mousePosition = { + x: 0, + y: 0, + }; + + this.tileSource = null; + this.renderer = null; + + this.zoom = 0; + // sf lat: 37.787946, lon: -122.407522 + // iceland lat: 64.124229, lon: -21.811552 + // rgbg + // lat: 49.019493, lon: 12.098341 + this.center = { + lat: 52.51298, + lon: 13.42012, + }; + + this.minZoom = null; + config = Object.assign(config, options); + } + + init() { + return new Promise((resolve) => { + if (!config.headless) { + this._initKeyboard(); + this._initMouse(); + } + this._initTileSource(); + this._initRenderer(); + this._draw(); + this.notify("Welcome to MapSCII! Use your cursors to navigate, a/z to zoom, q to quit."); + resolve(); + }); + } + + + _initTileSource() { + this.tileSource = new TileSource(); + this.tileSource.init(config.source); + } + + _initKeyboard() { + keypress(config.input); + if (config.input.setRawMode) { + config.input.setRawMode(true); + } + config.input.resume(); + + config.input.on('keypress', (ch, key) => this._onKey(key)); + } + + _initMouse() { + this.mouse = TermMouse({ + input: config.input, + output: config.output, + }); + this.mouse.start(); + + this.mouse.on('click', (event) => this._onClick(event)); + this.mouse.on('scroll', (event) => this._onMouseScroll(event)); + this.mouse.on('move', (event) => this._onMouseMove(event)); + } + + _initRenderer() { + this.renderer = new Renderer(config.output, this.tileSource); + this.renderer.loadStyleFile(config.styleFile); + + config.output.on('resize', () => { + this._resizeRenderer(); + this._draw(); + }); + + this._resizeRenderer() + this.zoom = (config.initialZoom !== null) ? config.initialZoom : this.minZoom; + } + + _resizeRenderer(cb) { + if (config.size) { + this.width = config.size.width; + this.height = config.size.height; + } else { + this.width = config.output.columns >> 1 << 2; + this.height = config.output.rows * 4 - 4; + } + + this.minZoom = 4-Math.log(4096/this.width)/Math.LN2; + + this.renderer.setSize(this.width, this.height); + } + + _updateMousePosition(event) { + const projected = { + x: (event.x-0.5)*2, + y: (event.y-0.5)*4, + }; + + const size = utils.tilesizeAtZoom(this.zoom); + const [dx, dy] = [projected.x-this.width/2, projected.y-this.height/2]; + + const z = utils.baseZoom(this.zoom); + const center = utils.ll2tile(this.center.lon, this.center.lat, z); + + this.mousePosition = utils.normalize(utils.tile2ll(center.x+(dx/size), center.y+(dy/size), z)); + } + + _onClick(event) { + if (event.x < 0 || event.x > this.width/2 || event.y < 0 || event.y > this.height/4) { + return; + } + this._updateMousePosition(event); + + if (this.mouseDragging && event.button === 'left') { + this.mouseDragging = false; + } else { + this.setCenter(this.mousePosition.lat, this.mousePosition.lon); + } + + this._draw(); + } + + _onMouseScroll(event) { + this._updateMousePosition(event); + // TODO: handle .x/y for directed zoom + this.zoomBy(config.zoomStep * (event.button === 'up' ? 1 : -1)); + this._draw(); + } + + _onMouseMove(event) { + if (event.x < 0 || event.x > this.width/2 || event.y < 0 || event.y > this.height/4) { + return; + } + if (config.mouseCallback && !config.mouseCallback(event)) { + return; + } + + // start dragging + if (event.button === 'left') { + if (this.mouseDragging) { + const dx = (this.mouseDragging.x-event.x)*2; + const dy = (this.mouseDragging.y-event.y)*4; + + const size = utils.tilesizeAtZoom(this.zoom); + + const newCenter = utils.tile2ll( + this.mouseDragging.center.x+(dx/size), + this.mouseDragging.center.y+(dy/size), + utils.baseZoom(this.zoom), + ); + + this.setCenter(newCenter.lat, newCenter.lon); + + this._draw(); + + } else { + this.mouseDragging = { + x: event.x, + y: event.y, + center: utils.ll2tile(this.center.lon, this.center.lat, utils.baseZoom(this.zoom)), + } + } + } + + this._updateMousePosition(event); + this.notify(this._getFooter()); + } + + _onKey(key) { + if (config.keyCallback && !config.keyCallback(key)) return; + if (!key || !key.name) return; + + // check if the pressed key is configured + let draw = true; + switch (key.name) { + case 'q': + if (config.quitCallback) { + config.quitCallback(); + } else { + process.exit(0); + } + break; + case 'a': + this.zoomBy(config.zoomStep); + break; + case 'z', 'y': + this.zoomBy(-config.zoomStep); + break; + case 'left': + this.moveBy(0, -8/Math.pow(2, this.zoom)); + break; + case 'right': + this.moveBy(0, 8/Math.pow(2, this.zoom)); + break; + case 'up': + this.moveBy(6/Math.pow(2, this.zoom), 0); + break; + case 'down': + this.moveBy(-6/Math.pow(2, this.zoom), 0); + break; + case 'c': + config.useBraille = !config.useBraille; + break; + default: + draw = false; + } + + if (draw !== null) { + this._draw(); + } + } + + _draw() { + this.renderer.draw(this.center, this.zoom).then((frame) => { + this._write(frame); + this.notify(this._getFooter()); + }).catch(() => { + this.notify('renderer is busy'); + }); + } + + _getFooter() { + // tile = utils.ll2tile(this.center.lon, this.center.lat, this.zoom); + // `tile: ${utils.digits(tile.x, 3)}, ${utils.digits(tile.x, 3)} `+ + + return `center: ${utils.digits(this.center.lat, 3)}, ${utils.digits(this.center.lon, 3)} `+ + `zoom: ${utils.digits(this.zoom, 2)} `+ + `mouse: ${utils.digits(this.mousePosition.lat, 3)}, ${utils.digits(this.mousePosition.lon, 3)} `; + } + + notify(text) { + config.onUpdate && config.onUpdate(); + if (!config.headless) { + this._write('\r\x1B[K' + text); + } + } + + _write(output) { + config.output.write(output); + } + + zoomBy(step) { + if (this.zoom+step < this.minZoom) { + return this.zoom = this.minZoom; + } + if (this.zoom+step > config.maxZoom) { + return this.zoom = config.maxZoom; + } + + this.zoom += step; + } + + moveBy(lat, lon) { + this.setCenter(this.center.lat+lat, this.center.lon+lon); + } + + setCenter(lat, lon) { + this.center = utils.normalize({ + lon: lon, + lat: lat, + }); + } +} + +module.exports = Mapscii;