mirror of
https://github.com/rastapasta/mapscii.git
synced 2024-11-25 01:23:58 +01:00
Non-perfect translation of Renderer.js
This commit is contained in:
parent
29b5b3ccb1
commit
283c245a88
342
src/Renderer.js
Normal file
342
src/Renderer.js
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
/*
|
||||||
|
termap - Terminal Map Viewer
|
||||||
|
by Michael Strassburger <codepoet@cpan.org>
|
||||||
|
|
||||||
|
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;
|
Loading…
Reference in New Issue
Block a user