mapscii/src/Renderer.js

336 wiersze
8.7 KiB
JavaScript

/*
termap - Terminal Map Viewer
by Michael Strassburger <codepoet@cpan.org>
The Console Vector Tile renderer - bäm!
*/
'use strict';
const x256 = require('x256');
const simplify = require('simplify-js');
const Canvas = require('./Canvas');
const LabelBuffer = require('./LabelBuffer');
const Styler = require('./Styler');
const utils = require('./utils');
const config = require('./config');
class Renderer {
constructor(output, tileSource, style) {
this.output = output;
this.tileSource = tileSource;
this.labelBuffer = new LabelBuffer();
this.styler = new Styler(style);
this.tileSource.useStyler(this.styler);
}
setSize(width, height) {
this.width = width;
this.height = height;
this.canvas = new Canvas(width, height);
}
async 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();
try {
let tiles = this._visibleTiles(center, zoom);
await Promise.all(tiles.map(async(tile) => {
await this._getTile(tile);
this._getTileFeatures(tile, zoom);
}));
await this._renderTiles(tiles);
return this._getFrame();
} catch(e) {
console.error(e);
} finally {
this.isDrawing = false;
this.lastDrawAt = Date.now();
}
}
_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,
position,
size: tileSize,
});
}
}
return tiles;
}
async _getTile(tile) {
tile.data = await this.tileSource.getTile(tile.xyz.z, tile.xyz.x, tile.xyz.y);
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) {
const labels = [];
if (tiles.length === 0) return;
const drawOrder = this._generateDrawOrder(tiles[0].xyz.z);
for (const layerId of drawOrder) {
for (const tile of tiles) {
const layer = tile.layers[layerId];
if (!layer) continue;
for (const feature of layer.features) {
// continue if feature.id and drawn[feature.id]
// drawn[feature.id] = true;
if (layerId.match(/label/)) {
labels.push({
tile,
feature,
scale: layer.scale
});
} else {
this._drawFeature(tile, feature, layer.scale);
}
}
}
}
labels.sort((a, b) => {
return a.feature.sorty - b.feature.sort;
});
for (const label of labels) {
this._drawFeature(label.tile, label.feature, label.scale);
}
}
_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;
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 genericSymbol = config.poiMarker;
const text = feature.label || config.poiMarker;
if (this._seen[text] && !genericSymbol) {
return false;
}
placed = false;
const pointsOfInterest = this._scaleAndReduce(tile, feature, feature.points, scale);
for (const point of pointsOfInterest) {
const x = point.x - text.length;
const layerMargin = (config.layers[feature.layer] || {}).margin;
const margin = layerMargin || 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 {
const cluster = (config.layers[feature.layer] || {}).cluster;
if (cluster && 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;
}
break;
}
}
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;