Merge af464f3c9bf5a498f889ace6c28a1e319a4fa4f7 into 4fe9a60a0c9da952dadc5214a9ca5c68c447fdf8

This commit is contained in:
Christian Paul 2024-11-03 16:02:54 +00:00 committed by GitHub
commit fd4792a62a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 2205 additions and 5376 deletions

View File

@ -1,11 +1,11 @@
module.exports = { export default {
"env": { "env": {
"es6": true, "es6": true,
"node": true, "node": true,
"jest": true "jest": true
}, },
"parserOptions": { "parserOptions": {
"ecmaVersion": 2018 "ecmaVersion": 2020
}, },
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",

View File

@ -1,9 +0,0 @@
language: node_js
node_js:
- "14"
- "16"
- "18"
script:
- npm run-script lint
- npm test

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

View File

@ -7,10 +7,14 @@
TODO: params parsing and so on TODO: params parsing and so on
#*/ #*/
'use strict'; import process from "node:process";
const config = require('./src/config');
const Mapscii = require('./src/Mapscii'); import config from './src/config.ts';
const argv = require('yargs') import Mapscii from './src/Mapscii.ts';
import yargs from 'yargs/yargs';
import { hideBin } from 'yargs/helpers';
const argv = yargs(hideBin(process.argv))
.option('latitude', { .option('latitude', {
alias: 'lat', alias: 'lat',
description: 'Latitude of initial centre', description: 'Latitude of initial centre',

6728
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,11 +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",
"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",
@ -15,9 +15,6 @@
"bin": { "bin": {
"mapscii": "./bin/mapscii.sh" "mapscii": "./bin/mapscii.sh"
}, },
"engines": {
"node": ">=10"
},
"keywords": [ "keywords": [
"map", "map",
"console", "console",
@ -30,23 +27,20 @@
"author": "Michael Straßburger <codepoet@cpan.org>", "author": "Michael Straßburger <codepoet@cpan.org>",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@mapbox/vector-tile": "^1.3.1", "@mapbox/mbtiles": "^0.12.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": "^3.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": "^15.4.1" "yargs": "^17.7.2"
},
"devDependencies": {
"eslint": "^7.8.1",
"eslint-plugin-jest": "^24.0.0",
"jest": "^26.4.2"
} }
} }

View File

@ -11,10 +11,10 @@
Will either be merged into node-drawille or become an own module at some point Will either be merged into node-drawille or become an own module at some point
*/ */
'use strict'; import { Buffer } from 'node:buffer';
const stringWidth = require('string-width'); import stringWidth from 'string-width';
const config = require('./config'); import config from './config.ts';
const utils = require('./utils'); import utils from './utils.ts';
const asciiMap = { const asciiMap = {
// '▬': [2+32, 4+64], // '▬': [2+32, 4+64],
@ -30,13 +30,20 @@ const asciiMap = {
const termReset = '\x1B[39;49m'; const termReset = '\x1B[39;49m';
class BrailleBuffer { class BrailleBuffer {
constructor(width, height) { private brailleMap: number[][];
private pixelBuffer: Buffer;
private charBuffer: string[];
private foregroundBuffer: Buffer;
private backgroundBuffer: Buffer;
private height: number;
private width: number;
private globalBackground: number | null;
private asciiToBraille: string[];
constructor(width: number, height: number) {
this.brailleMap = [[0x1, 0x8],[0x2, 0x10],[0x4, 0x20],[0x40, 0x80]]; this.brailleMap = [[0x1, 0x8],[0x2, 0x10],[0x4, 0x20],[0x40, 0x80]];
this.pixelBuffer = null; this.charBuffer = [];
this.charBuffer = null;
this.foregroundBuffer = null;
this.backgroundBuffer = null;
this.asciiToBraille = []; this.asciiToBraille = [];
@ -54,42 +61,42 @@ class BrailleBuffer {
this.clear(); this.clear();
} }
clear() { clear(): void {
this.pixelBuffer.fill(0); this.pixelBuffer.fill(0);
this.charBuffer = []; this.charBuffer = [];
this.foregroundBuffer.fill(0); this.foregroundBuffer.fill(0);
this.backgroundBuffer.fill(0); this.backgroundBuffer.fill(0);
} }
setGlobalBackground(background) { setGlobalBackground(background: number): void {
this.globalBackground = background; this.globalBackground = background;
} }
setBackground(x, y, color) { setBackground(x: number, y: number, color: number): void {
if (0 <= x && x < this.width && 0 <= y && y < this.height) { if (0 <= x && x < this.width && 0 <= y && y < this.height) {
const idx = this._project(x, y); const idx = this._project(x, y);
this.backgroundBuffer[idx] = color; this.backgroundBuffer[idx] = color;
} }
} }
setPixel(x, y, color) { setPixel(x: number, y: number, color: number): void {
this._locate(x, y, (idx, mask) => { this._locate(x, y, (idx, mask) => {
this.pixelBuffer[idx] |= mask; this.pixelBuffer[idx] |= mask;
this.foregroundBuffer[idx] = color; this.foregroundBuffer[idx] = color;
}); });
} }
unsetPixel(x, y) { unsetPixel(x: number, y: number): void {
this._locate(x, y, (idx, mask) => { this._locate(x, y, (idx, mask) => {
this.pixelBuffer[idx] &= ~mask; this.pixelBuffer[idx] &= ~mask;
}); });
} }
_project(x, y) { private _project(x: number, y: number): number {
return (x>>1) + (this.width>>1)*(y>>2); return (x>>1) + (this.width>>1)*(y>>2);
} }
_locate(x, y, cb) { private _locate(x: number, y: number, cb: (idx: number, mask: number) => unknown) {
if (!((0 <= x && x < this.width) && (0 <= y && y < this.height))) { if (!((0 <= x && x < this.width) && (0 <= y && y < this.height))) {
return; return;
} }
@ -98,57 +105,64 @@ class BrailleBuffer {
return cb(idx, mask); return cb(idx, mask);
} }
_mapBraille() { private _mapBraille(): string[] {
this.asciiToBraille = [' ']; this.asciiToBraille = [' '];
const masks = []; const masks: {
char: string,
covered: number,
mask: number,
}[] = [];
for (const char in asciiMap) { for (const char in asciiMap) {
const bits = asciiMap[char]; const bits: number[] | undefined = asciiMap[char];
if (!(bits instanceof Array)) continue; if (!(bits instanceof Array)) continue;
for (const mask of bits) { for (const mask of bits) {
masks.push({ masks.push({
mask: mask, char,
char: char, covered: 0,
mask,
}); });
} }
} }
//TODO Optimize this part //TODO Optimize this part
var i, k; let i: number, k: number;
const results = []; const results: string[] = [];
for (i = k = 1; k <= 255; i = ++k) { for (i = k = 1; k <= 255; i = ++k) {
const braille = (i & 7) + ((i & 56) << 1) + ((i & 64) >> 3) + (i & 128); const braille = (i & 7) + ((i & 56) << 1) + ((i & 64) >> 3) + (i & 128);
results.push(this.asciiToBraille[i] = masks.reduce((function(best, mask) { const char = masks.reduce((best, mask) => {
const covered = utils.population(mask.mask & braille); const covered = utils.population(mask.mask & braille);
if (!best || best.covered < covered) { if (!best || best.covered < covered) {
return { return {
char: mask.char, ...mask,
covered: covered, covered,
}; };
} else { } else {
return best; return best;
} }
}), void 0).char); }).char;
this.asciiToBraille[i] = char;
results.push(char);
} }
return results; return results;
} }
_termColor(foreground, background) { private _termColor(foreground: number, background: number): string {
background |= this.globalBackground; const actualBackground = background ?? this.globalBackground;
if (foreground && background) { if (foreground && actualBackground) {
return `\x1B[38;5;${foreground};48;5;${background}m`; return `\x1B[38;5;${foreground};48;5;${actualBackground}m`;
} else if (foreground) { } else if (foreground) {
return `\x1B[49;38;5;${foreground}m`; return `\x1B[49;38;5;${foreground}m`;
} else if (background) { } else if (actualBackground) {
return `\x1B[39;48;5;${background}m`; return `\x1B[39;48;5;${actualBackground}m`;
} else { } else {
return termReset; return termReset;
} }
} }
frame() { frame(): string {
const output = []; const output: string[] = [];
let currentColor = null; let currentColor: string | null = null;
let skip = 0; let skip = 0;
for (let y = 0; y < this.height/4; y++) { for (let y = 0; y < this.height/4; y++) {
@ -175,7 +189,7 @@ class BrailleBuffer {
} else { } else {
if (!skip) { if (!skip) {
if (config.useBraille) { if (config.useBraille) {
output.push(String.fromCharCode(0x2800+this.pixelBuffer[idx])); output.push(String.fromCharCode(0x2800 + this.pixelBuffer[idx]));
} else { } else {
output.push(this.asciiToBraille[this.pixelBuffer[idx]]); output.push(this.asciiToBraille[this.pixelBuffer[idx]]);
} }
@ -186,11 +200,11 @@ class BrailleBuffer {
} }
} }
output.push(termReset+config.delimeter); output.push(termReset + config.delimeter);
return output.join(''); return output.join('');
} }
setChar(char, x, y, color) { setChar(char: string, x: number, y: number, color: number): void {
if (0 <= x && x < this.width && 0 <= y && y < this.height) { if (0 <= x && x < this.width && 0 <= y && y < this.height) {
const idx = this._project(x, y); const idx = this._project(x, y);
this.charBuffer[idx] = char; this.charBuffer[idx] = char;
@ -198,7 +212,7 @@ class BrailleBuffer {
} }
} }
writeText(text, x, y, color, center = true) { writeText(text, x, y, color, center = true): void {
if (center) { if (center) {
x -= text.length/2+1; x -= text.length/2+1;
} }
@ -208,4 +222,4 @@ class BrailleBuffer {
} }
} }
module.exports = BrailleBuffer; export default BrailleBuffer;

View File

@ -10,13 +10,17 @@
Will most likely be turned into a stand alone module at some point Will most likely be turned into a stand alone module at some point
*/ */
'use strict'; import bresenham from 'bresenham';
const bresenham = require('bresenham'); import earcut from 'earcut';
const earcut = require('earcut'); import BrailleBuffer from './BrailleBuffer.ts';
const BrailleBuffer = require('./BrailleBuffer');
class Canvas { class Canvas {
constructor(width, height) { private buffer: BrailleBuffer;
private height: number;
private stack: unknown[] = [];
private width: number;
constructor(width: number, height: number) {
this.width = width; this.width = width;
this.height = height; this.height = height;
this.buffer = new BrailleBuffer(width, height); this.buffer = new BrailleBuffer(width, height);
@ -30,7 +34,7 @@ class Canvas {
this.buffer.clear(); this.buffer.clear();
} }
text(text, x, y, color, center = false) { text(text: string, x: number, y: number, color, center = false) {
this.buffer.writeText(text, x, y, color, center); this.buffer.writeText(text, x, y, color, center);
} }
@ -55,8 +59,8 @@ class Canvas {
} }
polygon(rings, color) { polygon(rings, color) {
const vertices = []; const vertices: number[] = [];
const holes = []; const holes: number[] = [];
for (const ring of rings) { for (const ring of rings) {
if (vertices.length) { if (vertices.length) {
if (ring.length < 3) continue; if (ring.length < 3) continue;
@ -73,7 +77,7 @@ class Canvas {
let triangles; let triangles;
try { try {
triangles = earcut(vertices, holes); triangles = earcut(vertices, holes);
} catch (error) { } catch {
return false; return false;
} }
for (let i = 0; i < triangles.length; i += 3) { for (let i = 0; i < triangles.length; i += 3) {
@ -85,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) => {
@ -145,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];
@ -154,18 +158,18 @@ 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);
const points = a.concat(b).concat(c).filter((point) => { const points = a.concat(b).concat(c).filter((point) => {
var ref; let ref;
return (0 <= (ref = point.y) && ref < this.height); return (0 <= (ref = point.y) && ref < this.height);
}).sort(function(a, b) { }).sort(function(a, b) {
if (a.y === b.y) { if (a.y === b.y) {
@ -197,6 +201,4 @@ class Canvas {
} }
} }
Canvas.prototype.stack = []; export default Canvas;
module.exports = Canvas;

View File

@ -1,57 +0,0 @@
/*
termap - Terminal Map Viewer
by Michael Strassburger <codepoet@cpan.org>
Using 2D spatial indexing to avoid overlapping labels and markers
and to find labels underneath a mouse cursor's position
*/
'use strict';
const RBush = require('rbush');
const stringWidth = require('string-width');
module.exports = class LabelBuffer {
constructor() {
this.tree = new RBush();
this.margin = 5;
}
clear() {
this.tree.clear();
}
project(x, y) {
return [Math.floor(x/2), Math.floor(y/4)];
}
writeIfPossible(text, x, y, feature, margin) {
margin = margin || this.margin;
const point = this.project(x, y);
if (this._hasSpace(text, point[0], point[1])) {
const data = this._calculateArea(text, point[0], point[1], margin);
data.feature = feature;
return this.tree.insert(data);
} else {
return false;
}
}
featuresAt(x, y) {
this.tree.search({minX: x, maxX: x, minY: y, maxY: y});
}
_hasSpace(text, x, y) {
return !this.tree.collides(this._calculateArea(text, x, y));
}
_calculateArea(text, x, y, margin = 0) {
return {
minX: x-margin,
minY: y-margin/2,
maxX: x+margin+stringWidth(text),
maxY: y+margin/2,
};
}
};

67
src/LabelBuffer.ts Normal file
View File

@ -0,0 +1,67 @@
/*
termap - Terminal Map Viewer
by Michael Strassburger <codepoet@cpan.org>
Using 2D spatial indexing to avoid overlapping labels and markers
and to find labels underneath a mouse cursor's position
*/
import RBush from 'rbush';
import stringWidth from 'string-width';
import { Feature } from './Renderer.ts';
export default class LabelBuffer {
private tree: RBush<{
minX: number,
minY: number,
maxX: number,
maxY: number,
feature: Feature,
}>;
private margin: number;
constructor() {
this.tree = new RBush();
this.margin = 5;
}
clear(): void {
this.tree.clear();
}
project(x: number, y: number): [number, number] {
return [Math.floor(x/2), Math.floor(y/4)];
}
writeIfPossible(text: string, x: number, y: number, feature, margin: number): boolean {
margin = margin || this.margin;
const point = this.project(x, y);
if (this._hasSpace(text, point[0], point[1])) {
this.tree.insert({
...this._calculateArea(text, point[0], point[1], margin),
feature,
});
return true;
} else {
return false;
}
}
// featuresAt(x: number, y: number) {
// return this.tree.search({minX: x, maxX: x, minY: y, maxY: y});
// }
private _hasSpace(text: string, x: number, y: number): boolean {
return !this.tree.collides(this._calculateArea(text, x, y));
}
private _calculateArea(text: string, x: number, y: number, margin = 0) {
return {
minX: x - margin,
minY: y - margin / 2,
maxX: x + margin + stringWidth(text),
maxY: y + margin / 2,
};
}
};

View File

@ -4,17 +4,43 @@
UI and central command center UI and central command center
*/ */
'use strict'; import fs from 'node:fs';
const fs = require('fs'); import process from "node:process";
const keypress = require('keypress');
const TermMouse = require('term-mouse');
const Renderer = require('./Renderer'); import keypress from 'keypress';
const TileSource = require('./TileSource'); import TermMouse from 'term-mouse';
const utils = require('./utils');
let config = require('./config'); import type Canvas from './Canvas.ts';
import Renderer from './Renderer.ts';
import TileSource from './TileSource.ts';
import utils from './utils.ts';
import globalConfig from './config.ts';
let config = globalConfig;
class Mapscii { class Mapscii {
private width: number | null;
private height: number | null;
private canvas: Canvas | null;
private mouse: TermMouse | null;
private mouseDragging: {
x: number,
y: number,
center: {
x: number,
y: number,
},
} | false;
private mousePosition: {lat: number, lon: number} | null;
private tileSource: TileSource | null;
private renderer: Renderer | null;
private zoom: number;
private minZoom: number | null;
private center: {
lat: number,
lon: number,
};
constructor(options) { constructor(options) {
this.width = null; this.width = null;
this.height = null; this.height = null;
@ -22,10 +48,7 @@ class Mapscii {
this.mouse = null; this.mouse = null;
this.mouseDragging = false; this.mouseDragging = false;
this.mousePosition = { this.mousePosition = null;
x: 0,
y: 0,
};
this.tileSource = null; this.tileSource = null;
this.renderer = null; this.renderer = null;
@ -52,22 +75,22 @@ 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);
} }
config.input.resume(); config.input.resume();
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,
@ -79,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);
@ -92,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;
@ -101,41 +124,41 @@ class Mapscii {
this.renderer.setSize(this.width, this.height); this.renderer.setSize(this.width, this.height);
} }
_colrow2ll(x, y) { 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,
}; };
const size = utils.tilesizeAtZoom(this.zoom); const size = utils.tilesizeAtZoom(this.zoom);
const [dx, dy] = [projected.x-this.width/2, projected.y-this.height/2]; const [dx, dy] = [projected.x - this.width / 2, projected.y - this.height / 2];
const z = utils.baseZoom(this.zoom); const z = utils.baseZoom(this.zoom);
const center = utils.ll2tile(this.center.lon, this.center.lat, z); const center = utils.ll2tile(this.center.lon, this.center.lat, z);
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) { 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;
} }
this._updateMousePosition(event); this._updateMousePosition(event);
if (this.mouseDragging && event.button === 'left') { if (this.mouseDragging && event.button === 'left') {
this.mouseDragging = false; this.mouseDragging = false;
} else { } else if (this.mousePosition !== null) {
this.setCenter(this.mousePosition.lat, this.mousePosition.lon); this.setCenter(this.mousePosition.lat, this.mousePosition.lon);
} }
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
@ -167,8 +190,8 @@ class Mapscii {
this._draw(); this._draw();
} }
_onMouseMove(event) { 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;
} }
if (config.mouseCallback && !config.mouseCallback(event)) { if (config.mouseCallback && !config.mouseCallback(event)) {
@ -178,14 +201,14 @@ class Mapscii {
// start dragging // start dragging
if (event.button === 'left') { if (event.button === 'left') {
if (this.mouseDragging) { if (this.mouseDragging) {
const dx = (this.mouseDragging.x-event.x)*2; const dx = (this.mouseDragging.x - event.x) * 2;
const dy = (this.mouseDragging.y-event.y)*4; const dy = (this.mouseDragging.y - event.y) * 4;
const size = utils.tilesizeAtZoom(this.zoom); const size = utils.tilesizeAtZoom(this.zoom);
const newCenter = utils.tile2ll( const newCenter = utils.tile2ll(
this.mouseDragging.center.x+(dx/size), this.mouseDragging.center.x + (dx / size),
this.mouseDragging.center.y+(dy/size), this.mouseDragging.center.y + (dy / size),
utils.baseZoom(this.zoom) utils.baseZoom(this.zoom)
); );
@ -206,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;
@ -255,8 +278,8 @@ 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());
}).catch(() => { }).catch(() => {
@ -264,13 +287,13 @@ 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)} `+
let footer = `center: ${utils.digits(this.center.lat, 3)}, ${utils.digits(this.center.lon, 3)} `; let footer = `center: ${utils.digits(this.center.lat, 3)}, ${utils.digits(this.center.lon, 3)} `;
footer += ` zoom: ${utils.digits(this.zoom, 2)} `; footer += ` zoom: ${utils.digits(this.zoom, 2)} `;
if (this.mousePosition.lat !== undefined) { if (this.mousePosition !== null) {
footer += ` mouse: ${utils.digits(this.mousePosition.lat, 3)}, ${utils.digits(this.mousePosition.lon, 3)} `; footer += ` mouse: ${utils.digits(this.mousePosition.lat, 3)}, ${utils.digits(this.mousePosition.lon, 3)} `;
} }
return footer; return footer;
@ -283,26 +306,25 @@ class Mapscii {
} }
} }
_write(output) { private _write(output): void {
config.output.write(output); config.output.write(output);
} }
zoomBy(step) { zoomBy(step: number): void {
if (this.zoom+step < this.minZoom) { if (this.zoom + step < this.minZoom) {
return this.zoom = this.minZoom; this.zoom = this.minZoom;
} else if (this.zoom + step > config.maxZoom) {
this.zoom = config.maxZoom;
} else {
this.zoom += step;
} }
if (this.zoom+step > config.maxZoom) {
return this.zoom = config.maxZoom;
}
this.zoom += step;
} }
moveBy(lat, lon) { moveBy(lat: number, lon: number): void {
this.setCenter(this.center.lat+lat, this.center.lon+lon); this.setCenter(this.center.lat + lat, this.center.lon + lon);
} }
setCenter(lat, lon) { setCenter(lat: number, lon: number): void {
this.center = utils.normalize({ this.center = utils.normalize({
lon: lon, lon: lon,
lat: lat, lat: lat,
@ -310,4 +332,4 @@ class Mapscii {
} }
} }
module.exports = Mapscii; export default Mapscii;

View File

@ -4,18 +4,61 @@
The Console Vector Tile renderer - bäm! The Console Vector Tile renderer - bäm!
*/ */
'use strict'; import RBush from 'rbush';
const x256 = require('x256'); import simplify from 'simplify-js';
const simplify = require('simplify-js'); import x256 from 'x256';
const Canvas = require('./Canvas'); import Canvas from './Canvas.ts';
const LabelBuffer = require('./LabelBuffer'); import LabelBuffer from './LabelBuffer.ts';
const Styler = require('./Styler'); import Styler from './Styler.ts';
const utils = require('./utils'); import utils from './utils.ts';
const config = require('./config'); import config from './config.ts';
import TileSource from './TileSource.ts';
import Tile from './Tile.ts';
export type Layer = {
extent?: unknown,
tree?: RBush<{
minX: number,
minY: number,
maxX: number,
maxY: number,
feature: Feature,
}>,
scale?: number,
features?: Feature[],
};
export type Layers = Record<string, Layer>;
export type Feature = {
properties: Record<string, unknown>,
// TODO `style` is likely incomplete and incorrect
style: {
type?: string,
},
sort: number,
sorty: number,
};
class Renderer { class Renderer {
constructor(output, tileSource, style) { private canvas: Canvas | undefined;
private height: number;
private labelBuffer: LabelBuffer;
private output: unknown;
private styler: Styler;
private tileSource: TileSource;
private width: number;
private terminal = {
CLEAR: '\x1B[2J',
MOVE: '\x1B[?6h',
};
private isDrawing = false;
private lastDrawAt = 0;
private tilePadding = 64;
constructor(output, tileSource: TileSource, style) {
this.output = output; this.output = output;
this.tileSource = tileSource; this.tileSource = tileSource;
this.labelBuffer = new LabelBuffer(); this.labelBuffer = new LabelBuffer();
@ -23,18 +66,18 @@ class Renderer {
this.tileSource.useStyler(this.styler); this.tileSource.useStyler(this.styler);
} }
setSize(width, height) { setSize(width: number, height: number) {
this.width = width; this.width = width;
this.height = height; this.height = height;
this.canvas = new Canvas(width, height); this.canvas = new Canvas(width, height);
} }
async draw(center, zoom) { async draw(center, zoom: number): Promise<string> {
if (this.isDrawing) return Promise.reject(); if (this.isDrawing) return Promise.reject();
this.isDrawing = true; this.isDrawing = true;
this.labelBuffer.clear(); this.labelBuffer.clear();
this._seen = {}; const seen = new Set<string>();
let ref; let ref;
const color = ((ref = this.styler.styleById['background']) !== null ? const color = ((ref = this.styler.styleById['background']) !== null ?
@ -43,32 +86,32 @@ class Renderer {
void 0 void 0
); );
if (color) { if (color) {
this.canvas.setBackground(x256(utils.hex2rgb(color))); this.canvas?.setBackground(x256(utils.hex2rgb(color)));
} }
this.canvas.clear(); this.canvas?.clear();
try { try {
let tiles = this._visibleTiles(center, zoom); const tiles = this._visibleTiles(center, zoom);
await Promise.all(tiles.map(async(tile) => { await Promise.all(tiles.map(async(tile) => {
await this._getTile(tile); await this._getTile(tile);
this._getTileFeatures(tile, zoom); this._getTileFeatures(tile, zoom);
})); }));
await this._renderTiles(tiles); await this._renderTiles(tiles, seen);
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);
const tiles = []; const tiles: Tile[] = [];
const tileSize = utils.tilesizeAtZoom(zoom); const tileSize = utils.tilesizeAtZoom(zoom);
for (let y = Math.floor(center.y) - 1; y <= Math.floor(center.y) + 1; y++) { for (let y = Math.floor(center.y) - 1; y <= Math.floor(center.y) + 1; y++) {
@ -102,14 +145,14 @@ class Renderer {
return tiles; return tiles;
} }
async _getTile(tile) { async _getTile(tile: Tile): Promise<Tile> {
tile.data = await this.tileSource.getTile(tile.xyz.z, tile.xyz.x, tile.xyz.y); tile.data = await this.tileSource.getTile(tile.xyz.z, tile.xyz.x, tile.xyz.y);
return tile; return tile;
} }
_getTileFeatures(tile, zoom) { 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];
@ -119,12 +162,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,
}), }),
}; };
} }
@ -132,8 +175,12 @@ class Renderer {
return tile; return tile;
} }
_renderTiles(tiles) { private _renderTiles(tiles, seen: Set<string>) {
const labels = []; const labels: {
tile: Tile,
feature: Feature,
scale: number,
}[] = [];
if (tiles.length === 0) return; if (tiles.length === 0) return;
const drawOrder = this._generateDrawOrder(tiles[0].xyz.z); const drawOrder = this._generateDrawOrder(tiles[0].xyz.z);
@ -148,10 +195,10 @@ class Renderer {
labels.push({ labels.push({
tile, tile,
feature, feature,
scale: layer.scale scale: layer.scale,
}); });
} else { } else {
this._drawFeature(tile, feature, layer.scale); this._drawFeature(tile, feature, layer.scale, seen);
} }
} }
} }
@ -162,11 +209,14 @@ class Renderer {
}); });
for (const label of labels) { for (const label of labels) {
this._drawFeature(label.tile, label.feature, label.scale); this._drawFeature(label.tile, label.feature, label.scale, seen);
} }
} }
_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;
@ -176,11 +226,14 @@ class Renderer {
return frame; return frame;
} }
featuresAt(x, y) { // featuresAt(x: number, y: number) {
return this.labelBuffer.featuresAt(x, y); // return this.labelBuffer.featuresAt(x, y);
} // }
_drawFeature(tile, feature, scale) { private _drawFeature(tile: Tile, feature, scale: number, seen: Set<string>): 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;
@ -212,7 +265,7 @@ class Renderer {
const genericSymbol = config.poiMarker; const genericSymbol = config.poiMarker;
const text = feature.label || config.poiMarker; const text = feature.label || config.poiMarker;
if (this._seen[text] && !genericSymbol) { if (seen.has(text) && !genericSymbol) {
return false; return false;
} }
@ -236,7 +289,7 @@ class Renderer {
} }
} }
if (placed) { if (placed) {
this._seen[text] = true; seen.add(text);
} }
break; break;
} }
@ -244,11 +297,11 @@ class Renderer {
return true; return true;
} }
_scaleAndReduce(tile, feature, points, scale, 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;
const scaled = []; const scaled: {x: number, y: number}[] = [];
const minX = -this.tilePadding; const minX = -this.tilePadding;
const minY = -this.tilePadding; const minY = -this.tilePadding;
@ -292,7 +345,7 @@ class Renderer {
} }
} }
_generateDrawOrder(zoom) { private _generateDrawOrder(zoom) {
if (zoom < 2) { if (zoom < 2) {
return [ return [
'admin', 'admin',
@ -321,15 +374,4 @@ class Renderer {
} }
} }
Renderer.prototype.terminal = { export default Renderer;
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;

View File

@ -8,13 +8,19 @@
Compiles layer filter instructions into a chain of true/false returning Compiles layer filter instructions into a chain of true/false returning
anonymous functions to improve rendering speed compared to realtime parsing. anonymous functions to improve rendering speed compared to realtime parsing.
*/ */
'use strict'; import type RBush from 'rbush';
import { Feature } from "./Renderer.ts";
class Styler { class Styler {
public styleById: Record<string, Record<string, unknown>>;
public styleByLayer: Record<string, unknown[]>;
private styleName: string;
constructor(style) { constructor(style) {
this.styleById = {}; this.styleById = {};
this.styleByLayer = {}; this.styleByLayer = {};
var base, name; let base, name;
this.styleName = style.name; this.styleName = style.name;
if (style.constants) { if (style.constants) {
this._replaceConstants(style.constants, style.layers); this._replaceConstants(style.constants, style.layers);
@ -40,7 +46,7 @@ class Styler {
} }
} }
getStyleFor(layer, feature) { getStyleFor(layer: string, feature: Feature): unknown | false {
if (!this.styleByLayer[layer]) { if (!this.styleByLayer[layer]) {
return false; return false;
} }
@ -54,7 +60,7 @@ class Styler {
return false; return false;
} }
_replaceConstants(constants, tree) { 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) {
@ -73,7 +79,7 @@ class Styler {
} }
//TODO Better translation of the long cases. //TODO Better translation of the long cases.
_compileFilter(filter) { 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':
@ -130,4 +136,4 @@ class Styler {
} }
} }
module.exports = Styler; export default Styler;

View File

@ -4,22 +4,43 @@
Handling of and access to single VectorTiles Handling of and access to single VectorTiles
*/ */
'use strict'; import { Buffer } from 'node:buffer';
const VectorTile = require('@mapbox/vector-tile').VectorTile; import zlib from 'node:zlib';
const Protobuf = require('pbf'); import { VectorTile } from '@mapbox/vector-tile';
const zlib = require('zlib'); import Protobuf from 'pbf';
const RBush = require('rbush'); import RBush from 'rbush';
const x256 = require('x256'); import x256 from 'x256';
const config = require('./config'); import config from './config.ts';
const utils = require('./utils'); import utils from './utils.ts';
import Styler from './Styler.ts';
import { Layers } from './Renderer.ts';
class Tile { class Tile {
constructor(styler) { public layers: Layers = {};
private styler: Styler;
private tile: VectorTile;
public size: number;
public position: {
x: number,
y: number,
};
public xyz: {
x: number,
y: number,
z: number,
};
public zoom: number;
public data: {
layers: unknown;
};
constructor(styler: Styler) {
this.styler = styler; this.styler = styler;
} }
load(buffer) { load(buffer: Buffer) {
return this._unzipIfNeeded(buffer).then((buffer) => { return this._unzipIfNeeded(buffer).then((buffer) => {
return this._loadTile(buffer); return this._loadTile(buffer);
}).then(() => { }).then(() => {
@ -29,11 +50,11 @@ class Tile {
}); });
} }
_loadTile(buffer) { private _loadTile(buffer: Buffer) {
this.tile = new VectorTile(new Protobuf(buffer)); this.tile = new VectorTile(new Protobuf(buffer));
} }
_unzipIfNeeded(buffer) { private _unzipIfNeeded(buffer: Buffer): Promise<Buffer> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (this._isGzipped(buffer)) { if (this._isGzipped(buffer)) {
zlib.gunzip(buffer, (err, data) => { zlib.gunzip(buffer, (err, data) => {
@ -48,16 +69,16 @@ class Tile {
}); });
} }
_isGzipped(buffer) { private _isGzipped(buffer): boolean {
return buffer.slice(0, 2).indexOf(Buffer.from([0x1f, 0x8b])) === 0; return buffer.slice(0, 2).indexOf(Buffer.from([0x1f, 0x8b])) === 0;
} }
_loadLayers() { private _loadLayers(): Layers {
const layers = {}; const layers: Layers = {};
const colorCache = {}; 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
@ -121,11 +142,12 @@ class Tile {
return this.layers = layers; return this.layers = layers;
} }
_addBoundaries(deep, data) { private _addBoundaries(deep: boolean, data) {
let minX = 2e308; // 2e307 is a high number that's not Infinity.
let maxX = -2e308; let minX = 2e307;
let minY = 2e308; let maxX = -2e307;
let maxY = -2e308; let minY = 2e307;
let maxY = -2e307;
const points = (deep ? data.points[0] : data.points); const points = (deep ? data.points[0] : data.points);
for (const p of points) { for (const p of points) {
if (p.x < minX) minX = p.x; if (p.x < minX) minX = p.x;
@ -140,28 +162,26 @@ class Tile {
return data; return data;
} }
_reduceGeometry(feature, factor) { // private _reduceGeometry(feature: Feature, factor: number): {x: number, y: number}[][] {
const results = []; // 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 = []; // 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;
module.exports = Tile;

View File

@ -1,12 +1,12 @@
'use strict'; import { describe, expect, test } from 'jest';
const TileSource = require('./TileSource'); import TileSource, { Mode } from './TileSource.ts';
describe('TileSource', () => { describe('TileSource', () => {
describe('with a HTTP source', () => { describe('with a HTTP source', () => {
test('sets the mode to 3', async () => { test('sets the mode to 3', async () => {
const tileSource = new TileSource(); const tileSource = new TileSource();
await tileSource.init('http://mapscii.me/'); await tileSource.init('http://mapscii.me/');
expect(tileSource.mode).toBe(3); expect(tileSource.mode).toBe(Mode.HTTP);
}); });
}); });
}); });

View File

@ -6,32 +6,41 @@
* remote TileServer * remote TileServer
* local MBTiles and VectorTiles * local MBTiles and VectorTiles
*/ */
'use strict'; import fs from 'node:fs';
const fs = require('fs'); import path from 'node:path';
const path = require('path'); import fetch from 'node-fetch';
const fetch = require('node-fetch'); import envPaths from 'env-paths';
const envPaths = require('env-paths');
const paths = envPaths('mapscii'); const paths = envPaths('mapscii');
const Tile = require('./Tile'); import config from './config.ts';
const config = require('./config'); import Tile from './Tile.ts';
import Styler from './Styler.ts';
// https://github.com/mapbox/node-mbtiles has native build dependencies (sqlite3) // https://github.com/mapbox/node-mbtiles has native build dependencies (sqlite3)
// To maximize MapSCIIs compatibility, MBTiles support must be manually added via // To maximize MapSCIIs compatibility, MBTiles support must be manually added via
// $> npm install -g @mapbox/mbtiles // $> npm install -g @mapbox/mbtiles
let MBTiles = null; // let MBTiles = null;
try { // try {
MBTiles = require('@mapbox/mbtiles'); // MBTiles = await import('@mapbox/mbtiles');
} catch (err) {void 0;} // } catch {void 0;}
import MBTiles from '@mapbox/mbtiles';
const modes = { export enum Mode {
MBTiles: 1, MBTiles = 1,
VectorTile: 2, VectorTile = 2,
HTTP: 3, HTTP = 3,
}; };
class TileSource { class TileSource {
init(source) { private source: string;
private cache: Record<string, unknown>;
private cacheSize: number;
private cached: unknown[];
public mode: Mode | null;
private mbtiles: MBTiles | null;
private styler: Styler;
init(source: string): void {
this.source = source; this.source = source;
this.cache = {}; this.cache = {};
@ -47,23 +56,23 @@ class TileSource {
this._initPersistence(); this._initPersistence();
} }
this.mode = modes.HTTP; this.mode = Mode.HTTP;
} else if (this.source.endsWith('.mbtiles')) { } else if (this.source.endsWith('.mbtiles')) {
if (!MBTiles) { if (!MBTiles) {
throw new Error('MBTiles support must be installed with following command: \'npm install -g @mapbox/mbtiles\''); throw new Error('MBTiles support must be installed with following command: \'npm install -g @mapbox/mbtiles\'');
} }
this.mode = modes.MBTiles; this.mode = Mode.MBTiles;
this.loadMBTiles(source); this.loadMBTiles(source);
} else { } else {
throw new Error('source type isn\'t supported yet'); throw new Error('source type isn\'t supported yet');
} }
} }
loadMBTiles(source) { loadMBTiles(source): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
new MBTiles(source, (err, mbtiles) => { new MBTiles(`${source}?mode=ro`, (err, mbtiles) => {
if (err) { if (err) {
reject(err); reject(err);
} }
@ -77,7 +86,7 @@ class TileSource {
this.styler = styler; this.styler = styler;
} }
getTile(z, x, y) { getTile(z: number, x: number, y: number) {
if (!this.mode) { if (!this.mode) {
throw new Error('no TileSource defined'); throw new Error('no TileSource defined');
} }
@ -88,21 +97,21 @@ class TileSource {
} }
if (this.cached.length > this.cacheSize) { if (this.cached.length > this.cacheSize) {
const overflow = Math.abs(this.cacheSize - this.cache.length); const overflow = Math.abs(this.cacheSize - this.cached.length);
for (const tile in this.cached.splice(0, overflow)) { for (const tile in this.cached.splice(0, overflow)) {
delete this.cache[tile]; delete this.cache[tile];
} }
} }
switch (this.mode) { switch (this.mode) {
case modes.MBTiles: case Mode.MBTiles:
return this._getMBTile(z, x, y); return this._getMBTile(z, x, y);
case modes.HTTP: case Mode.HTTP:
return this._getHTTP(z, x, y); return this._getHTTP(z, x, y);
} }
} }
_getHTTP(z, x, y) { private _getHTTP(z: number, x: number, y: number) {
let promise; let promise;
const persistedTile = this._getPersited(z, x, y); const persistedTile = this._getPersited(z, x, y);
if (config.persistDownloadedTiles && persistedTile) { if (config.persistDownloadedTiles && persistedTile) {
@ -122,7 +131,7 @@ class TileSource {
}); });
} }
_getMBTile(z, x, y) { private _getMBTile(z: number, x: number, y: number) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.mbtiles.getTile(z, x, y, (err, buffer) => { this.mbtiles.getTile(z, x, y, (err, buffer) => {
if (err) { if (err) {
@ -133,7 +142,7 @@ class TileSource {
}); });
} }
_createTile(z, x, y, buffer) { private _createTile(z: number, x: number, y: number, buffer) {
const name = [z, x, y].join('-'); const name = [z, x, y].join('-');
this.cached.push(name); this.cached.push(name);
@ -141,30 +150,30 @@ class TileSource {
return tile.load(buffer); return tile.load(buffer);
} }
_initPersistence() { private _initPersistence() {
try { try {
this._createFolder(paths.cache); this._createFolder(paths.cache);
} catch (error) { } catch {
config.persistDownloadedTiles = false; config.persistDownloadedTiles = false;
} }
} }
_persistTile(z, x, y, buffer) { private _persistTile(z, x, y, buffer) {
const zoom = z.toString(); const zoom = z.toString();
this._createFolder(path.join(paths.cache, zoom)); this._createFolder(path.join(paths.cache, zoom));
const filePath = path.join(paths.cache, zoom, `${x}-${y}.pbf`); const filePath = path.join(paths.cache, zoom, `${x}-${y}.pbf`);
return fs.writeFile(filePath, buffer, () => null); return fs.writeFile(filePath, buffer, () => null);
} }
_getPersited(z, x, y) { private _getPersited(z: number, x: number, y: number) {
try { try {
return fs.readFileSync(path.join(paths.cache, z.toString(), `${x}-${y}.pbf`)); return fs.readFileSync(path.join(paths.cache, z.toString(), `${x}-${y}.pbf`));
} catch (error) { } catch {
return false; return false;
} }
} }
_createFolder(path) { private _createFolder(path: string): true {
try { try {
fs.mkdirSync(path); fs.mkdirSync(path);
return true; return true;
@ -175,4 +184,4 @@ class TileSource {
} }
} }
module.exports = TileSource; export default TileSource;

View File

@ -1,4 +1,6 @@
module.exports = { import process from "node:process";
export default {
language: 'en', language: 'en',
// TODO: adapt to osm2vectortiles successor openmaptiles v3) // TODO: adapt to osm2vectortiles successor openmaptiles v3)
@ -7,7 +9,7 @@ module.exports = {
//source: __dirname+"/../mbtiles/regensburg.mbtiles", //source: __dirname+"/../mbtiles/regensburg.mbtiles",
styleFile: __dirname+'/../styles/dark.json', styleFile: import.meta.dirname+'/../styles/dark.json',
initialZoom: null, initialZoom: null,
maxZoom: 18, maxZoom: 18,

View File

@ -4,15 +4,14 @@
methods used all around methods used all around
*/ */
'use strict'; import config from './config.ts';
const config = require('./config');
const constants = { const constants = {
RADIUS: 6378137, RADIUS: 6378137,
}; };
const utils = { const utils = {
clamp: (num, min, max) => { clamp: (num: number, min: number, max: number): number => {
if (num <= min) { if (num <= min) {
return min; return min;
} else if (num >= max) { } else if (num >= max) {
@ -22,20 +21,20 @@ const utils = {
} }
}, },
baseZoom: (zoom) => { baseZoom: (zoom: number): number => {
return Math.min(config.tileRange, Math.max(0, Math.floor(zoom))); return Math.min(config.tileRange, Math.max(0, Math.floor(zoom)));
}, },
tilesizeAtZoom: (zoom) => { tilesizeAtZoom: (zoom: number): number => {
return config.projectSize * Math.pow(2, zoom-utils.baseZoom(zoom)); return config.projectSize * Math.pow(2, zoom-utils.baseZoom(zoom));
}, },
deg2rad: (angle) => { deg2rad: (angle: number): number => {
// (angle / 180) * Math.PI // (angle / 180) * Math.PI
return angle * 0.017453292519943295; return angle * 0.017453292519943295;
}, },
ll2tile: (lon, lat, zoom) => { ll2tile: (lon: number, lat: number, zoom: number): {x: number, y: number, z: number} => {
return { return {
x: (lon+180)/360*Math.pow(2, zoom), x: (lon+180)/360*Math.pow(2, zoom),
y: (1-Math.log(Math.tan(lat*Math.PI/180)+1/Math.cos(lat*Math.PI/180))/Math.PI)/2*Math.pow(2, zoom), y: (1-Math.log(Math.tan(lat*Math.PI/180)+1/Math.cos(lat*Math.PI/180))/Math.PI)/2*Math.pow(2, zoom),
@ -43,7 +42,7 @@ const utils = {
}; };
}, },
tile2ll: (x, y, zoom) => { tile2ll: (x: number, y: number, zoom: number): {lat: number, lon: number} => {
const n = Math.PI - 2*Math.PI*y/Math.pow(2, zoom); const n = Math.PI - 2*Math.PI*y/Math.pow(2, zoom);
return { return {
@ -52,35 +51,38 @@ const utils = {
}; };
}, },
metersPerPixel: (zoom, lat = 0) => { metersPerPixel: (zoom: number, lat = 0): number => {
return (Math.cos(lat * Math.PI/180) * 2 * Math.PI * constants.RADIUS) / (256 * Math.pow(2, zoom)); return (Math.cos(lat * Math.PI/180) * 2 * Math.PI * constants.RADIUS) / (256 * Math.pow(2, zoom));
}, },
hex2rgb: (color) => { hex2rgb: (color: unknown): [r: number, g: number, b: number] => {
if (typeof color !== 'string') return [255, 0, 0]; if (typeof color !== 'string') {
return [255, 0, 0];
}
if (!/^#[a-fA-F0-9]{3,6}$/.test(color)) { if (!/^#[a-fA-F0-9]{3,6}$/.test(color)) {
throw new Error(`${color} isn't a supported hex color`); throw new Error(`${color} isn't a supported hex color`);
} }
color = color.substr(1); const decimal = parseInt(color.substring(1), 16);
const decimal = parseInt(color, 16);
if (color.length === 3) { if (color.length === 4) {
const rgb = [decimal>>8, (decimal>>4)&15, decimal&15]; const rgb = [decimal>>8, (decimal>>4)&15, decimal&15];
return rgb.map((c) => { return [
return c + (c<<4); rgb[0] + (rgb[0]<<4),
}); rgb[1] + (rgb[1]<<4),
rgb[2] + (rgb[2]<<4),
];
} else { } else {
return [(decimal>>16)&255, (decimal>>8)&255, decimal&255]; return [(decimal>>16)&255, (decimal>>8)&255, decimal&255];
} }
}, },
digits: (number, digits) => { digits: (number: number, digits: number): number => {
return Math.floor(number*Math.pow(10, digits))/Math.pow(10, digits); return Math.floor(number*Math.pow(10, digits))/Math.pow(10, digits);
}, },
normalize: (ll) => { normalize: (ll: {lat: number, lon: number}): {lat: number, lon: number} => {
if (ll.lon < -180) ll.lon += 360; if (ll.lon < -180) ll.lon += 360;
if (ll.lon > 180) ll.lon -= 360; if (ll.lon > 180) ll.lon -= 360;
@ -90,7 +92,7 @@ const utils = {
return ll; return ll;
}, },
population: (val) => { population: (val: number): number => {
let bits = 0; let bits = 0;
while (val > 0) { while (val > 0) {
bits += val & 1; bits += val & 1;
@ -100,4 +102,4 @@ const utils = {
}, },
}; };
module.exports = utils; export default utils;