mirror of
https://github.com/rastapasta/mapscii.git
synced 2025-06-20 17:57:40 +02:00
Merge af464f3c9bf5a498f889ace6c28a1e319a4fa4f7 into 4fe9a60a0c9da952dadc5214a9ca5c68c447fdf8
This commit is contained in:
commit
fd4792a62a
@ -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",
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
language: node_js
|
|
||||||
node_js:
|
|
||||||
- "14"
|
|
||||||
- "16"
|
|
||||||
- "18"
|
|
||||||
|
|
||||||
script:
|
|
||||||
- npm run-script lint
|
|
||||||
- npm test
|
|
@ -1,6 +1,6 @@
|
|||||||
# MapSCII - The Whole World In Your Console. [](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"></a>
|
<a href="https://asciinema.org/a/117813?autoplay=1" target="_blank"></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
|
||||||
|
@ -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',
|
6650
package-lock.json
generated
6650
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
@ -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;
|
|
@ -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
67
src/LabelBuffer.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
@ -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) {
|
||||||
if (this.zoom+step > config.maxZoom) {
|
this.zoom = config.maxZoom;
|
||||||
return this.zoom = config.maxZoom;
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
this.zoom += step;
|
this.zoom += step;
|
||||||
}
|
}
|
||||||
|
|
||||||
moveBy(lat, lon) {
|
|
||||||
this.setCenter(this.center.lat+lat, this.center.lon+lon);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setCenter(lat, lon) {
|
moveBy(lat: number, lon: number): void {
|
||||||
|
this.setCenter(this.center.lat + lat, this.center.lon + 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;
|
@ -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;
|
|
@ -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;
|
@ -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;
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
@ -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 MapSCII’s compatibility, MBTiles support must be manually added via
|
// To maximize MapSCII’s 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;
|
@ -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,
|
@ -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;
|
Loading…
x
Reference in New Issue
Block a user