Suggest Bun and Deno 2; Upgrade most packages; Remove babel, ESLint and jest as dependencies

This commit is contained in:
Christian Paul 2024-11-03 16:25:18 +01:00
parent 81a7fdea12
commit ec0877830a
9 changed files with 396 additions and 4740 deletions

View File

@ -1,6 +1,6 @@
# MapSCII - The Whole World In Your Console. [![Build Status](https://travis-ci.com/rastapasta/mapscii.svg?branch=master)](https://travis-ci.com/rastapasta/mapscii) # MapSCII - The Whole World In Your Console.
A node.js based [Vector Tile](http://wiki.openstreetmap.org/wiki/Vector_tiles) to [Braille](http://www.fileformat.info/info/unicode/block/braille_patterns/utf8test.htm) and [ASCII](https://de.wikipedia.org/wiki/American_Standard_Code_for_Information_Interchange) renderer for [xterm](https://en.wikipedia.org/wiki/Xterm)-compatible terminals. A TypeScript-based [Vector Tile](http://wiki.openstreetmap.org/wiki/Vector_tiles) to [Braille](http://www.fileformat.info/info/unicode/block/braille_patterns/utf8test.htm) and [ASCII](https://de.wikipedia.org/wiki/American_Standard_Code_for_Information_Interchange) renderer for [xterm](https://en.wikipedia.org/wiki/Xterm)-compatible terminals.
<a href="https://asciinema.org/a/117813?autoplay=1" target="_blank">![asciicast](https://cloud.githubusercontent.com/assets/1259904/25480718/497a64e2-2b4a-11e7-9cf0-ed52ee0b89c0.png)</a> <a href="https://asciinema.org/a/117813?autoplay=1" target="_blank">![asciicast](https://cloud.githubusercontent.com/assets/1259904/25480718/497a64e2-2b4a-11e7-9cf0-ed52ee0b89c0.png)</a>
@ -22,7 +22,6 @@ If you're on Windows, use the open source telnet client [PuTTY](https://www.chia
* Work offline and discover local [VectorTile](https://github.com/mapbox/vector-tile-spec)/[MBTiles](https://github.com/mapbox/mbtiles-spec) * Work offline and discover local [VectorTile](https://github.com/mapbox/vector-tile-spec)/[MBTiles](https://github.com/mapbox/mbtiles-spec)
* Compatible with most Linux and OSX terminals * Compatible with most Linux and OSX terminals
* Highly optimized algorithms for a smooth experience * Highly optimized algorithms for a smooth experience
* 100% pure JavaScript! :sunglasses:
## How to run it locally ## How to run it locally
@ -36,7 +35,7 @@ npx mapscii
### With npm ### With npm
If you haven't already got Node.js >= version 10, then [go get it](http://nodejs.org/). If you haven't already got Bun or Deno 2, then [go get it](https://deno.com/).
``` ```
npm install -g mapscii npm install -g mapscii

4920
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,12 +2,11 @@
"name": "mapscii", "name": "mapscii",
"version": "0.3.1", "version": "0.3.1",
"description": "MapSCII is a Braille & ASCII world map renderer for your console, based on OpenStreetMap", "description": "MapSCII is a Braille & ASCII world map renderer for your console, based on OpenStreetMap",
"main": "main.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"lint": "eslint src", "lint": "deno lint",
"start": "node main", "start": "deno --allow-all main.ts",
"test": "jest" "test": "deno test"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -16,9 +15,6 @@
"bin": { "bin": {
"mapscii": "./bin/mapscii.sh" "mapscii": "./bin/mapscii.sh"
}, },
"engines": {
"node": ">=20"
},
"keywords": [ "keywords": [
"map", "map",
"console", "console",
@ -32,24 +28,19 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@mapbox/mbtiles": "^0.12.1", "@mapbox/mbtiles": "^0.12.1",
"@mapbox/vector-tile": "^1.3.1", "@mapbox/vector-tile": "^2.0.3",
"@types/rbush": "^4.0.0",
"bresenham": "0.0.4", "bresenham": "0.0.4",
"earcut": "^2.2.2", "earcut": "^3.0.0",
"env-paths": "^2.2.0", "env-paths": "^3.0.0",
"keypress": "^0.2.1", "keypress": "^0.2.1",
"node-fetch": "^2.6.1", "node-fetch": "^3.3.2",
"pbf": "^3.2.1", "pbf": "^4.0.1",
"rbush": "^4.0.1", "rbush": "^4.0.1",
"simplify-js": "^1.2.4", "simplify-js": "^1.2.4",
"string-width": "^4.2.0", "string-width": "^7.2.0",
"term-mouse": "^0.2.2", "term-mouse": "^0.2.2",
"x256": "0.0.2", "x256": "0.0.2",
"yargs": "^17.7.2" "yargs": "^17.7.2"
},
"devDependencies": {
"@babel/preset-typescript": "^7.25.7",
"eslint": "^7.8.1",
"eslint-plugin-jest": "^24.0.0",
"jest": "^29.7.0"
} }
} }

View File

@ -89,13 +89,13 @@ class Canvas {
return true; return true;
} }
_polygonExtract(vertices, pointId) { private _polygonExtract(vertices, pointId) {
return [vertices[pointId * 2], vertices[pointId * 2 + 1]]; return [vertices[pointId * 2], vertices[pointId * 2 + 1]];
} }
// Inspired by Alois Zingl's "The Beauty of Bresenham's Algorithm" // Inspired by Alois Zingl's "The Beauty of Bresenham's Algorithm"
// -> http://members.chello.at/~easyfilter/bresenham.html // -> http://members.chello.at/~easyfilter/bresenham.html
_line(x0, y0, x1, y1, width, color) { private _line(x0, y0, x1, y1, width, color) {
// Fall back to width-less bresenham algorithm if we dont have a width // Fall back to width-less bresenham algorithm if we dont have a width
if (!(width = Math.max(0, width - 1))) { if (!(width = Math.max(0, width - 1))) {
return bresenham(x0, y0, x1, y1, (x, y) => { return bresenham(x0, y0, x1, y1, (x, y) => {
@ -149,7 +149,7 @@ class Canvas {
/* eslint-enable */ /* eslint-enable */
} }
_filledRectangle(x, y, width, height, color) { private _filledRectangle(x, y, width, height, color) {
const pointA = [x, y]; const pointA = [x, y];
const pointB = [x + width, y]; const pointB = [x + width, y];
const pointC = [x, y + height]; const pointC = [x, y + height];
@ -158,12 +158,12 @@ class Canvas {
this._filledTriangle(pointC, pointB, pointD, color); this._filledTriangle(pointC, pointB, pointD, color);
} }
_bresenham(pointA, pointB) { private _bresenham(pointA, pointB) {
return bresenham(pointA[0], pointA[1], pointB[0], pointB[1]); return bresenham(pointA[0], pointA[1], pointB[0], pointB[1]);
} }
// Draws a filled triangle // Draws a filled triangle
_filledTriangle(pointA, pointB, pointC, color) { private _filledTriangle(pointA, pointB, pointC, color) {
const a = this._bresenham(pointB, pointC); const a = this._bresenham(pointB, pointC);
const b = this._bresenham(pointA, pointC); const b = this._bresenham(pointA, pointC);
const c = this._bresenham(pointA, pointB); const c = this._bresenham(pointA, pointB);

View File

@ -7,9 +7,16 @@
*/ */
import RBush from 'rbush'; import RBush from 'rbush';
import stringWidth from 'string-width'; import stringWidth from 'string-width';
import { Feature } from './Renderer.ts';
export default class LabelBuffer { export default class LabelBuffer {
private tree: RBush; private tree: RBush<{
minX: number,
minY: number,
maxX: number,
maxY: number,
feature: Feature,
}>;
private margin: number; private margin: number;
constructor() { constructor() {
@ -31,29 +38,30 @@ export default class LabelBuffer {
const point = this.project(x, y); const point = this.project(x, y);
if (this._hasSpace(text, point[0], point[1])) { if (this._hasSpace(text, point[0], point[1])) {
return this.tree.insert({ this.tree.insert({
...this._calculateArea(text, point[0], point[1], margin), ...this._calculateArea(text, point[0], point[1], margin),
feature, feature,
}); });
return true;
} else { } else {
return false; return false;
} }
} }
featuresAt(x: number, y: number): void { // featuresAt(x: number, y: number) {
this.tree.search({minX: x, maxX: x, minY: y, maxY: y}); // return this.tree.search({minX: x, maxX: x, minY: y, maxY: y});
} // }
_hasSpace(text: string, x: number, y: number): boolean { private _hasSpace(text: string, x: number, y: number): boolean {
return !this.tree.collides(this._calculateArea(text, x, y)); return !this.tree.collides(this._calculateArea(text, x, y));
} }
_calculateArea(text: string, x: number, y: number, margin = 0) { private _calculateArea(text: string, x: number, y: number, margin = 0) {
return { return {
minX: x-margin, minX: x - margin,
minY: y-margin / 2, minY: y - margin / 2,
maxX: x+margin + stringWidth(text), maxX: x + margin + stringWidth(text),
maxY: y+margin / 2, maxY: y + margin / 2,
}; };
} }
}; };

View File

@ -75,12 +75,12 @@ class Mapscii {
} }
_initTileSource() { private _initTileSource() {
this.tileSource = new TileSource(); this.tileSource = new TileSource();
this.tileSource.init(config.source); this.tileSource.init(config.source);
} }
_initKeyboard() { private _initKeyboard() {
keypress(config.input); keypress(config.input);
if (config.input.setRawMode) { if (config.input.setRawMode) {
config.input.setRawMode(true); config.input.setRawMode(true);
@ -90,7 +90,7 @@ class Mapscii {
config.input.on('keypress', (_ch, key) => this._onKey(key)); config.input.on('keypress', (_ch, key) => this._onKey(key));
} }
_initMouse() { private _initMouse() {
this.mouse = TermMouse({ this.mouse = TermMouse({
input: config.input, input: config.input,
output: config.output, output: config.output,
@ -102,7 +102,7 @@ class Mapscii {
this.mouse.on('move', (event) => this._onMouseMove(event)); this.mouse.on('move', (event) => this._onMouseMove(event));
} }
_initRenderer() { private _initRenderer() {
const style = JSON.parse(fs.readFileSync(config.styleFile, 'utf8')); const style = JSON.parse(fs.readFileSync(config.styleFile, 'utf8'));
this.renderer = new Renderer(config.output, this.tileSource, style); this.renderer = new Renderer(config.output, this.tileSource, style);
@ -115,7 +115,7 @@ class Mapscii {
this.zoom = (config.initialZoom !== null) ? config.initialZoom : this.minZoom; this.zoom = (config.initialZoom !== null) ? config.initialZoom : this.minZoom;
} }
_resizeRenderer() { private _resizeRenderer() {
this.width = config.size && config.size.width ? config.size.width * 2 : config.output.columns >> 1 << 2; this.width = config.size && config.size.width ? config.size.width * 2 : config.output.columns >> 1 << 2;
this.height = config.size && config.size.height ? config.size.height * 4 : config.output.rows * 4 - 4; this.height = config.size && config.size.height ? config.size.height * 4 : config.output.rows * 4 - 4;
@ -124,7 +124,7 @@ class Mapscii {
this.renderer.setSize(this.width, this.height); this.renderer.setSize(this.width, this.height);
} }
_colrow2ll(x: number, y: number): {lat: number, lon: number} { private _colrow2ll(x: number, y: number): {lat: number, lon: number} {
const projected = { const projected = {
x: (x-0.5)*2, x: (x-0.5)*2,
y: (y-0.5)*4, y: (y-0.5)*4,
@ -139,11 +139,11 @@ class Mapscii {
return utils.normalize(utils.tile2ll(center.x + (dx / size), center.y + (dy / size), z)); return utils.normalize(utils.tile2ll(center.x + (dx / size), center.y + (dy / size), z));
} }
_updateMousePosition(event: {x: number, y: number}): void { private _updateMousePosition(event: {x: number, y: number}): void {
this.mousePosition = this._colrow2ll(event.x, event.y); this.mousePosition = this._colrow2ll(event.x, event.y);
} }
_onClick(event) { private _onClick(event) {
if (event.x < 0 || event.x > this.width / 2 || event.y < 0 || event.y > this.height / 4) { if (event.x < 0 || event.x > this.width / 2 || event.y < 0 || event.y > this.height / 4) {
return; return;
} }
@ -158,7 +158,7 @@ class Mapscii {
this._draw(); this._draw();
} }
_onMouseScroll(event) { private _onMouseScroll(event) {
this._updateMousePosition(event); this._updateMousePosition(event);
// the location of the pointer, where we want to zoom toward // the location of the pointer, where we want to zoom toward
@ -190,7 +190,7 @@ class Mapscii {
this._draw(); this._draw();
} }
_onMouseMove(event: {button: string, x: number, y: number}) { private _onMouseMove(event: {button: string, x: number, y: number}) {
if (event.x < 0 || event.x > this.width / 2 || event.y < 0 || event.y > this.height / 4) { if (event.x < 0 || event.x > this.width / 2 || event.y < 0 || event.y > this.height / 4) {
return; return;
} }
@ -229,7 +229,7 @@ class Mapscii {
this.notify(this._getFooter()); this.notify(this._getFooter());
} }
_onKey(key) { private _onKey(key) {
if (config.keyCallback && !config.keyCallback(key)) return; if (config.keyCallback && !config.keyCallback(key)) return;
if (!key || !key.name) return; if (!key || !key.name) return;
@ -278,7 +278,7 @@ class Mapscii {
} }
} }
_draw() { private _draw() {
this.renderer?.draw(this.center, this.zoom).then((frame) => { this.renderer?.draw(this.center, this.zoom).then((frame) => {
this._write(frame); this._write(frame);
this.notify(this._getFooter()); this.notify(this._getFooter());
@ -287,7 +287,7 @@ class Mapscii {
}); });
} }
_getFooter() { private _getFooter() {
// tile = utils.ll2tile(this.center.lon, this.center.lat, this.zoom); // tile = utils.ll2tile(this.center.lon, this.center.lat, this.zoom);
// `tile: ${utils.digits(tile.x, 3)}, ${utils.digits(tile.x, 3)} `+ // `tile: ${utils.digits(tile.x, 3)}, ${utils.digits(tile.x, 3)} `+
@ -306,7 +306,7 @@ class Mapscii {
} }
} }
_write(output): void { private _write(output): void {
config.output.write(output); config.output.write(output);
} }

View File

@ -17,12 +17,26 @@ import TileSource from './TileSource.ts';
import Tile from './Tile.ts'; import Tile from './Tile.ts';
export type Layer = { export type Layer = {
extent: unknown, extent?: unknown,
tree: RBush, tree?: RBush<{
minX: number,
minY: number,
maxX: number,
maxY: number,
feature: Feature,
}>,
scale?: number,
features?: Feature[],
}; };
export type Layers = Record<string, Layer>; export type Layers = Record<string, Layer>;
export type Feature = { export type Feature = {
properties: Record<string, unknown>, properties: Record<string, unknown>,
// TODO `style` is likely incomplete and incorrect
style: {
type?: string,
},
sort: number,
sorty: number,
}; };
class Renderer { class Renderer {
@ -86,15 +100,15 @@ class Renderer {
})); }));
await this._renderTiles(tiles); await this._renderTiles(tiles);
return this._getFrame(); return this._getFrame();
} catch(e) { } catch (err) {
console.error(e); console.error(err);
} finally { } finally {
this.isDrawing = false; this.isDrawing = false;
this.lastDrawAt = Date.now(); this.lastDrawAt = Date.now();
} }
} }
_visibleTiles(center, zoom) { private _visibleTiles(center, zoom) {
const z = utils.baseZoom(zoom); const z = utils.baseZoom(zoom);
center = utils.ll2tile(center.lon, center.lat, z); center = utils.ll2tile(center.lon, center.lat, z);
@ -137,9 +151,9 @@ class Renderer {
return tile; return tile;
} }
_getTileFeatures(tile: Tile, zoom: number): Tile { private _getTileFeatures(tile: Tile, zoom: number): Tile {
const position = tile.position; const position = tile.position;
const layers = {}; const layers: Layers = {};
const drawOrder = this._generateDrawOrder(zoom); const drawOrder = this._generateDrawOrder(zoom);
for (const layerId of drawOrder) { for (const layerId of drawOrder) {
const layer = (tile.data.layers || {})[layerId]; const layer = (tile.data.layers || {})[layerId];
@ -149,12 +163,12 @@ class Renderer {
const scale = layer.extent / utils.tilesizeAtZoom(zoom); const scale = layer.extent / utils.tilesizeAtZoom(zoom);
layers[layerId] = { layers[layerId] = {
scale: scale, scale,
features: layer.tree.search({ features: layer.tree.search({
minX: -position.x * scale, minX: -position.x * scale,
minY: -position.y * scale, minY: -position.y * scale,
maxX: (this.width - position.x) * scale, maxX: (this.width - position.x) * scale,
maxY: (this.height - position.y) * scale maxY: (this.height - position.y) * scale,
}), }),
}; };
} }
@ -162,11 +176,11 @@ class Renderer {
return tile; return tile;
} }
_renderTiles(tiles) { private _renderTiles(tiles) {
const labels: { const labels: {
tile: Tile, tile: Tile,
feature: Feature, feature: Feature,
scale: unknown, scale: number,
}[] = []; }[] = [];
if (tiles.length === 0) return; if (tiles.length === 0) return;
@ -200,7 +214,10 @@ class Renderer {
} }
} }
_getFrame() { private _getFrame(): string {
if (!this.canvas) {
return '';
}
let frame = ''; let frame = '';
if (!this.lastDrawAt) { if (!this.lastDrawAt) {
frame += this.terminal.CLEAR; frame += this.terminal.CLEAR;
@ -210,11 +227,14 @@ class Renderer {
return frame; return frame;
} }
featuresAt(x: number, y: number): Feature[] { // featuresAt(x: number, y: number) {
return this.labelBuffer.featuresAt(x, y); // return this.labelBuffer.featuresAt(x, y);
} // }
_drawFeature(tile, feature, scale: number): boolean { private _drawFeature(tile: Tile, feature, scale: number): boolean {
if (!this.canvas) {
return false;
}
let points, placed; let points, placed;
if (feature.style.minzoom && tile.zoom < feature.style.minzoom) { if (feature.style.minzoom && tile.zoom < feature.style.minzoom) {
return false; return false;
@ -278,7 +298,7 @@ class Renderer {
return true; return true;
} }
_scaleAndReduce(tile: Tile, feature: Feature, points: {x: number, y: number}[], scale: number, filter = true) { private _scaleAndReduce(tile: Tile, feature: Feature, points: {x: number, y: number}[], scale: number, filter = true) {
let lastX; let lastX;
let lastY; let lastY;
let outside; let outside;
@ -326,7 +346,7 @@ class Renderer {
} }
} }
_generateDrawOrder(zoom) { private _generateDrawOrder(zoom) {
if (zoom < 2) { if (zoom < 2) {
return [ return [
'admin', 'admin',

View File

@ -60,7 +60,7 @@ class Styler {
return false; return false;
} }
_replaceConstants(constants, tree: RBush): void { private _replaceConstants(constants, tree: RBush): void {
for (const id in tree) { for (const id in tree) {
const node = tree[id]; const node = tree[id];
switch (typeof node) { switch (typeof node) {
@ -79,7 +79,7 @@ class Styler {
} }
//TODO Better translation of the long cases. //TODO Better translation of the long cases.
_compileFilter(filter): (feature: Feature) => boolean { private _compileFilter(filter): (feature: Feature) => boolean {
let filters; let filters;
switch (filter != null ? filter[0] : void 0) { switch (filter != null ? filter[0] : void 0) {
case 'all': case 'all':

View File

@ -14,10 +14,10 @@ import x256 from 'x256';
import config from './config.ts'; import config from './config.ts';
import utils from './utils.ts'; import utils from './utils.ts';
import Styler from './Styler.ts'; import Styler from './Styler.ts';
import { Feature, Layers } from './Renderer.ts'; import { Layers } from './Renderer.ts';
class Tile { class Tile {
public layers: Layers; public layers: Layers = {};
private styler: Styler; private styler: Styler;
private tile: VectorTile; private tile: VectorTile;
@ -32,7 +32,9 @@ class Tile {
z: number, z: number,
}; };
public zoom: number; public zoom: number;
public data: unknown; public data: {
layers: unknown;
};
constructor(styler: Styler) { constructor(styler: Styler) {
this.styler = styler; this.styler = styler;
@ -76,7 +78,7 @@ class Tile {
const colorCache: Record<string, string> = {}; const colorCache: Record<string, string> = {};
for (const name in this.tile.layers) { for (const name in this.tile.layers) {
const layer = this.tile.layers[name]; const layer = this.tile.layers[name];
const nodes = []; const nodes: unknown[] = [];
//continue if name is 'water' //continue if name is 'water'
for (let i = 0; i < layer.length; i++) { for (let i = 0; i < layer.length; i++) {
// TODO: caching of similar attributes to avoid looking up the style each time // TODO: caching of similar attributes to avoid looking up the style each time
@ -159,28 +161,26 @@ class Tile {
return data; return data;
} }
private _reduceGeometry(feature: Feature, factor: number): {x: number, y: number}[][] { // private _reduceGeometry(feature: Feature, factor: number): {x: number, y: number}[][] {
const results: {x: number, y: number}[][] = []; // const results: {x: number, y: number}[][] = [];
const geometries = feature.loadGeometry(); // const geometries = feature.loadGeometry();
for (const points of geometries) { // for (const points of geometries) {
const reduced: {x: number, y: number}[] = []; // const reduced: {x: number, y: number}[] = [];
let last; // let last;
for (const point of points) { // for (const point of points) {
const p = { // const p = {
x: Math.floor(point.x / factor), // x: Math.floor(point.x / factor),
y: Math.floor(point.y / factor) // y: Math.floor(point.y / factor)
}; // };
if (last && last.x === p.x && last.y === p.y) { // if (last && last.x === p.x && last.y === p.y) {
continue; // continue;
} // }
reduced.push(last = p); // reduced.push(last = p);
} // }
results.push(reduced); // results.push(reduced);
} // }
return results; // return results;
} // }
} }
Tile.prototype.layers = {};
export default Tile; export default Tile;