From 283c245a88ef8f540f4c2fc119466fc514b49312 Mon Sep 17 00:00:00 2001 From: Christian Paul Date: Fri, 22 Dec 2017 21:33:10 -0800 Subject: [PATCH] Non-perfect translation of Renderer.js --- src/Renderer.js | 342 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 src/Renderer.js diff --git a/src/Renderer.js b/src/Renderer.js new file mode 100644 index 0000000..e505155 --- /dev/null +++ b/src/Renderer.js @@ -0,0 +1,342 @@ +/* + termap - Terminal Map Viewer + by Michael Strassburger + + The Console Vector Tile renderer - bäm! +*/ +'use strict'; +const Promise = require('bluebird'); +const x256 = require('x256'); +const simplify = require('simplify-js'); + +const Canvas = require('./Canvas'); +const LabelBuffer = require('./LabelBuffer'); +const Styler = require('./Styler'); +const Tile = require('./Tile'); +const utils = require('./utils'); +const config = require('./config'); + +class Renderer { + constructor(output, tileSource) { + this.output = output; + this.tileSource = tileSource; + this.labelBuffer = new LabelBuffer(); + } + + loadStyleFile(file) { + this.styler = new Styler(file); + this.tileSource.useStyler(this.styler); + } + + setSize(width, height) { + this.width = width; + this.height = height; + this.canvas = new Canvas(width, height); + } + + draw(center, zoom) { + if (this.isDrawing) return Promise.reject(); + this.isDrawing = true; + + this.labelBuffer.clear(); + this._seen = {}; + + let ref; + const color = ((ref = this.styler.styleById['background']) !== null ? + ref.paint['background-color'] + : + void 0 + ); + if (color) { + this.canvas.setBackground(x256(utils.hex2rgb(color))); + } + + this.canvas.clear(); + + return Promise.resolve(this._visibleTiles(center, zoom)).map((tile) => { + return this._getTile(tile); + }).map((tile) => { + return this._getTileFeatures(tile, zoom); + }).then((tiles) => { + return this._renderTiles(tiles); + }).then(() => { + return this._getFrame(); + }).catch(function(e) { + return console.log(e); + }).finally((frame) => { + this.isDrawing = false; + this.lastDrawAt = Date.now(); + return frame; + }); + } + + _visibleTiles(center, zoom) { + const z = utils.baseZoom(zoom); + center = utils.ll2tile(center.lon, center.lat, z); + + const tiles = []; + const tileSize = utils.tilesizeAtZoom(zoom); + + for (let y = Math.floor(center.y) - 1; y <= Math.floor(center.y) + 1; y++) { + for (let x = Math.floor(center.x) - 1; x <= Math.floor(center.x) + 1; x++) { + const tile = {x, y, z}; + const position = { + x: this.width / 2 - (center.x - tile.x) * tileSize, + y: this.height / 2 - (center.y - tile.y) * tileSize, + }; + + const gridSize = Math.pow(2, z); + + tile.x %= gridSize; + + if (tile.x < 0) { + tile.x = z === 0 ? 0 : tile.x + gridSize; + } + + if (tile.y < 0 || tile.y >= gridSize || position.x + tileSize < 0 || position.y + tileSize < 0 || position.x > this.width || position.y > this.height) { + continue; + } + + tiles.push({ + xyz: tile, + zoom: zoom, + position: position, + size: tileSize, + }); + } + } + return tiles; + } + + _getTile(tile) { + return this.tileSource + .getTile(tile.xyz.z, tile.xyz.x, tile.xyz.y) + .then((data) => { + tile.data = data; + return tile; + }); + } + + _getTileFeatures(tile, zoom) { + const position = tile.position; + const layers = {}; + const drawOrder = this._generateDrawOrder(zoom); + for (const layerId of drawOrder) { + const layer = (tile.data.layers || {})[layerId]; + if (!layer) { + continue; + } + + const scale = layer.extent / utils.tilesizeAtZoom(zoom); + layers[layerId] = { + scale: scale, + features: layer.tree.search({ + minX: -position.x * scale, + minY: -position.y * scale, + maxX: (this.width - position.x) * scale, + maxY: (this.height - position.y) * scale + }), + }; + } + tile.layers = layers; + return tile; + } + + _renderTiles(tiles) { + var drawn, feature, i, j, k, l, label, labels, layer, layerId, len, len1, len2, len3, ref, ref1, tile; + drawn = {}; + labels = []; + ref = this._generateDrawOrder(tiles[0].xyz.z); + for (i = 0, len = ref.length; i < len; i++) { + layerId = ref[i]; + for (j = 0, len1 = tiles.length; j < len1; j++) { + tile = tiles[j]; + if (!(layer = tile.layers[layerId])) { + continue; + } + ref1 = layer.features; + for (k = 0, len2 = ref1.length; k < len2; k++) { + feature = ref1[k]; + // continue if feature.id and drawn[feature.id] + // drawn[feature.id] = true + if (layerId.match(/label/)) { + labels.push({ + tile: tile, + feature: feature, + scale: layer.scale + }); + } else { + this._drawFeature(tile, feature, layer.scale); + } + } + } + } + labels.sort(function(a, b) { + return a.feature.sorty - b.feature.sort; + }); + const results = []; + for (const label of labels) { + results.push(this._drawFeature(label.tile, label.feature, label.scale)); + } + return results; + } + + _getFrame() { + let frame = ''; + if (!this.lastDrawAt) { + frame += this.terminal.CLEAR; + } + frame += this.terminal.MOVE; + frame += this.canvas.frame(); + return frame; + } + + featuresAt(x, y) { + return this.labelBuffer.featuresAt(x, y); + } + + _drawFeature(tile, feature, scale) { + let points, placed, genericSymbol; + if (feature.style.minzoom && tile.zoom < feature.style.minzoom) { + return false; + } else if (feature.style.maxzoom && tile.zoom > feature.style.maxzoom) { + return false; + } + + switch (feature.style.type) { + case 'line': + let width = feature.style.paint['line-width']; + if (width instanceof Object) { + // TODO: apply the correct zoom based value + width = width.stops[0][1]; + } + points = this._scaleAndReduce(tile, feature, feature.points, scale); + if (points.length) { + this.canvas.polyline(points, feature.color, width); + } + break; + case 'fill': + points = feature.points.map((p) => { + return this._scaleAndReduce(tile, feature, p, scale, false); + }); + this.canvas.polygon(points, feature.color); + break; + case 'symbol': + const text = feature.label || (genericSymbol = config.poiMarker); + + if (this._seen[text] && !genericSymbol) { + return false; + } + + placed = false; + const points2 = this._scaleAndReduce(tile, feature, feature.points, scale); + for (const point of points2) { + let ref1, ref2; + const x = point.x - text.length; + const margin = ((ref1 = config.layers[feature.layer]) != null ? ref1.margin : void 0) || config.labelMargin; + if (this.labelBuffer.writeIfPossible(text, x, point.y, feature, margin)) { + this.canvas.text(text, x, point.y, feature.color); + placed = true; + break; + } else if (((ref2 = config.layers[feature.layer]) != null ? ref2.cluster : void 0) && this.labelBuffer.writeIfPossible(config.poiMarker, point.x, point.y, feature, 3)) { + this.canvas.text(config.poiMarker, point.x, point.y, feature.color); + placed = true; + break; + } + } + if (placed) { + this._seen[text] = true; + } + } + return true; + } + + _scaleAndReduce(tile, feature, points, scale, filter = true) { + let lastX; + let lastY; + let outside; + const scaled = []; + + const minX = -this.tilePadding; + const minY = -this.tilePadding; + const maxX = this.width + this.tilePadding; + const maxY = this.height + this.tilePadding; + + for (const point of points) { + const x = Math.floor(tile.position.x + (point.x / scale)); + const y = Math.floor(tile.position.y + (point.y / scale)); + if (lastX === x && lastY === y) { + continue; + } + lastY = y; + lastX = x; + if (filter) { + if (x < minX || x > maxX || y < minY || y > maxY) { + if (outside) { + continue; + } + outside = true; + } else { + if (outside) { + outside = null; + scaled.push({x: lastX, y: lastY}); + } + } + } + scaled.push({x, y}); + } + if (feature.style.type !== 'symbol') { + if (scaled.length < 2) { + return []; + } + if (config.simplifyPolylines) { + return simplify(scaled, .5, true); + } else { + return scaled; + } + } else { + return scaled; + } + } + + _generateDrawOrder(zoom) { + if (zoom < 2) { + return [ + 'admin', + 'water', + 'country_label', + 'marine_label', + ]; + } else { + return [ + 'landuse', + 'water', + 'marine_label', + 'building', + 'road', + 'admin', + 'country_label', + 'state_label', + 'water_label', + 'place_label', + 'rail_station_label', + 'poi_label', + 'road_label', + 'housenum_label', + ]; + } + } +} + +Renderer.prototype.terminal = { + CLEAR: '\x1B[2J', + MOVE: '\x1B[?6h', +}; + +Renderer.prototype.isDrawing = false; +Renderer.prototype.lastDrawAt = 0; +Renderer.prototype.labelBuffer = null; +Renderer.prototype.tileSource = null; +Renderer.prototype.tilePadding = 64; + +module.exports = Renderer;