Merge a45870de45703a332153c961f7ff3b667b712ee1 into 4fe9a60a0c9da952dadc5214a9ca5c68c447fdf8

This commit is contained in:
Christian Paul 2023-02-26 17:57:28 +01:00 committed by GitHub
commit 4648bea0a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 6798 additions and 3682 deletions

View File

@ -1,22 +1,23 @@
module.exports = {
"env": {
"es6": true,
"node": true,
"es2021": true,
"jest": true
},
"parserOptions": {
"ecmaVersion": 2018
},
"extends": [
"eslint:recommended",
"plugin:jest/recommended"
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
"indent": [
"error",
2,
{
"SwitchCase": 1
"SwitchCase": 1
}
],
"linebreak-style": [
@ -33,4 +34,4 @@ module.exports = {
"always"
]
}
};
}

10
main.js
View File

@ -7,11 +7,11 @@
TODO: params parsing and so on
#*/
'use strict';
const config = require('./src/config');
const Mapscii = require('./src/Mapscii');
const argv = require('yargs')
.option('latitude', {
import config from './src/config.js';
import Mapscii from './src/Mapscii.js';
import yargs from 'yargs';
const argv = yargs().option('latitude', {
alias: 'lat',
description: 'Latitude of initial centre',
default: config.initialLat,

9887
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,10 +3,11 @@
"version": "0.3.1",
"description": "MapSCII is a Braille & ASCII world map renderer for your console, based on OpenStreetMap",
"main": "main.js",
"type": "module",
"scripts": {
"lint": "eslint src",
"start": "node main",
"test": "jest"
"test": "NODE_OPTIONS=--experimental-vm-modules jest"
},
"repository": {
"type": "git",
@ -32,21 +33,21 @@
"dependencies": {
"@mapbox/vector-tile": "^1.3.1",
"bresenham": "0.0.4",
"earcut": "^2.2.2",
"env-paths": "^2.2.0",
"earcut": "^2.2.4",
"env-paths": "^3.0.0",
"keypress": "^0.2.1",
"node-fetch": "^2.6.1",
"node-fetch": "^3.3.0",
"pbf": "^3.2.1",
"rbush": "^3.0.1",
"simplify-js": "^1.2.4",
"string-width": "^4.2.0",
"string-width": "^5.1.2",
"term-mouse": "^0.2.2",
"x256": "0.0.2",
"yargs": "^15.4.1"
"yargs": "^17.7.1"
},
"devDependencies": {
"eslint": "^7.8.1",
"eslint-plugin-jest": "^24.0.0",
"jest": "^26.4.2"
"eslint": "^8.35.0",
"eslint-plugin-jest": "^27.2.1",
"jest": "^29.4.3"
}
}

View File

@ -11,10 +11,9 @@
Will either be merged into node-drawille or become an own module at some point
*/
'use strict';
const stringWidth = require('string-width');
const config = require('./config');
const utils = require('./utils');
import stringWidth from 'string-width';
import config from './config.js';
import * as utils from './utils.js';
const asciiMap = {
// '▬': [2+32, 4+64],
@ -29,7 +28,7 @@ const asciiMap = {
};
const termReset = '\x1B[39;49m';
class BrailleBuffer {
export default class BrailleBuffer {
constructor(width, height) {
this.brailleMap = [[0x1, 0x8],[0x2, 0x10],[0x4, 0x20],[0x40, 0x80]];
@ -114,9 +113,8 @@ class BrailleBuffer {
}
//TODO Optimize this part
var i, k;
const results = [];
for (i = k = 1; k <= 255; i = ++k) {
for (let i = 1; i <= 255; i++) {
const braille = (i & 7) + ((i & 56) << 1) + ((i & 64) >> 3) + (i & 128);
results.push(this.asciiToBraille[i] = masks.reduce((function(best, mask) {
const covered = utils.population(mask.mask & braille);
@ -207,5 +205,3 @@ class BrailleBuffer {
}
}
}
module.exports = BrailleBuffer;

10
src/BrailleBuffer.spec.js Normal file
View File

@ -0,0 +1,10 @@
import BrailleBuffer from './BrailleBuffer.js';
const termReset = '\x1B[39;49m';
describe('BrailleBuffer', () => {
test('starts a frame with term reset characters', async () => {
const brailleBuffer = new BrailleBuffer(1, 1);
expect(brailleBuffer.frame().startsWith(termReset)).toBe(true);
});
});

View File

@ -10,12 +10,11 @@
Will most likely be turned into a stand alone module at some point
*/
'use strict';
const bresenham = require('bresenham');
const earcut = require('earcut');
const BrailleBuffer = require('./BrailleBuffer');
import bresenham from 'bresenham';
import earcut from 'earcut';
import BrailleBuffer from './BrailleBuffer.js';
class Canvas {
export default class Canvas {
constructor(width, height) {
this.width = width;
this.height = height;
@ -164,9 +163,8 @@ class Canvas {
const b = this._bresenham(pointA, pointC);
const c = this._bresenham(pointA, pointB);
const points = a.concat(b).concat(c).filter((point) => {
var ref;
return (0 <= (ref = point.y) && ref < this.height);
const points = a.concat(b).concat(c).filter(({y}) => {
return 0 <= y && y < this.height;
}).sort(function(a, b) {
if (a.y === b.y) {
return a.x - b.x;
@ -198,5 +196,3 @@ class Canvas {
}
Canvas.prototype.stack = [];
module.exports = Canvas;

View File

@ -5,11 +5,10 @@
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');
import RBush from 'rbush';
import stringWidth from 'string-width';
module.exports = class LabelBuffer {
export default class LabelBuffer {
constructor() {
this.tree = new RBush();
@ -54,4 +53,4 @@ module.exports = class LabelBuffer {
maxY: y+margin/2,
};
}
};
}

View File

@ -4,17 +4,16 @@
UI and central command center
*/
'use strict';
const fs = require('fs');
const keypress = require('keypress');
const TermMouse = require('term-mouse');
import fsPromises from 'fs/promises';
import keypress from 'keypress';
import TermMouse from 'term-mouse';
const Renderer = require('./Renderer');
const TileSource = require('./TileSource');
const utils = require('./utils');
let config = require('./config');
import Renderer from './Renderer.js';
import TileSource from './TileSource.js';
import * as utils from './utils.js';
import MapsciiConfig from './config.js';
class Mapscii {
export default class Mapscii {
constructor(options) {
this.width = null;
this.height = null;
@ -32,45 +31,48 @@ class Mapscii {
this.zoom = 0;
this.minZoom = null;
config = Object.assign(config, options);
this.config = {
...MapsciiConfig,
...options,
};
this.center = {
lat: config.initialLat,
lon: config.initialLon
lat: this.config.initialLat,
lon: this.config.initialLon,
};
}
async init() {
if (!config.headless) {
if (!this.config.headless) {
this._initKeyboard();
this._initMouse();
}
this._initTileSource();
this._initRenderer();
this._draw();
await this._initTileSource();
await this._initRenderer();
await this._draw();
this.notify('Welcome to MapSCII! Use your cursors to navigate, a/z to zoom, q to quit.');
}
_initTileSource() {
async _initTileSource() {
this.tileSource = new TileSource();
this.tileSource.init(config.source);
await this.tileSource.init(this.config.source);
}
_initKeyboard() {
keypress(config.input);
if (config.input.setRawMode) {
config.input.setRawMode(true);
keypress(this.config.input);
if (this.config.input.setRawMode) {
this.config.input.setRawMode(true);
}
config.input.resume();
this.config.input.resume();
config.input.on('keypress', (ch, key) => this._onKey(key));
this.config.input.on('keypress', (ch, key) => this._onKey(key));
}
_initMouse() {
this.mouse = TermMouse({
input: config.input,
output: config.output,
input: this.config.input,
output: this.config.output,
});
this.mouse.start();
@ -79,22 +81,22 @@ class Mapscii {
this.mouse.on('move', (event) => this._onMouseMove(event));
}
_initRenderer() {
const style = JSON.parse(fs.readFileSync(config.styleFile, 'utf8'));
this.renderer = new Renderer(config.output, this.tileSource, style);
async _initRenderer() {
const style = JSON.parse(await fsPromises.readFile(this.config.styleFile, 'utf8'));
this.renderer = new Renderer(this.tileSource, style);
config.output.on('resize', () => {
this.config.output.on('resize', () => {
this._resizeRenderer();
this._draw();
});
this._resizeRenderer();
this.zoom = (config.initialZoom !== null) ? config.initialZoom : this.minZoom;
this.zoom = (this.config.initialZoom !== null) ? this.config.initialZoom : this.minZoom;
}
_resizeRenderer() {
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.width = this.config.size && this.config.size.width ? this.config.size.width * 2 : this.config.output.columns >> 1 << 2;
this.height = this.config.size && this.config.size.height ? this.config.size.height * 4 : this.config.output.rows * 4 - 4;
this.minZoom = 4-Math.log(4096/this.width)/Math.LN2;
@ -142,7 +144,7 @@ class Mapscii {
const targetMouseLonLat = this._colrow2ll(event.x, event.y);
// zoom toward the center
this.zoomBy(config.zoomStep * (event.button === 'up' ? 1 : -1));
this.zoomBy(this.config.zoomStep * (event.button === 'up' ? 1 : -1));
// the location the pointer ended up after zooming
const offsetMouseLonLat = this._colrow2ll(event.x, event.y);
@ -171,7 +173,7 @@ class Mapscii {
if (event.x < 0 || event.x > this.width/2 || event.y < 0 || event.y > this.height/4) {
return;
}
if (config.mouseCallback && !config.mouseCallback(event)) {
if (this.config.mouseCallback && !this.config.mouseCallback(event)) {
return;
}
@ -207,25 +209,25 @@ class Mapscii {
}
_onKey(key) {
if (config.keyCallback && !config.keyCallback(key)) return;
if (this.config.keyCallback && !this.config.keyCallback(key)) return;
if (!key || !key.name) return;
// check if the pressed key is configured
let draw = true;
switch (key.name) {
case 'q':
if (config.quitCallback) {
config.quitCallback();
if (this.config.quitCallback) {
this.config.quitCallback();
} else {
process.exit(0);
}
break;
case 'a':
this.zoomBy(config.zoomStep);
this.zoomBy(this.config.zoomStep);
break;
case 'y':
case 'z':
this.zoomBy(-config.zoomStep);
this.zoomBy(-this.config.zoomStep);
break;
case 'left':
case 'h':
@ -244,7 +246,7 @@ class Mapscii {
this.moveBy(-6/Math.pow(2, this.zoom), 0);
break;
case 'c':
config.useBraille = !config.useBraille;
this.config.useBraille = !this.config.useBraille;
break;
default:
draw = false;
@ -255,13 +257,14 @@ class Mapscii {
}
}
_draw() {
this.renderer.draw(this.center, this.zoom).then((frame) => {
async _draw() {
try {
const frame = await this.renderer.draw(this.center, this.zoom);
this._write(frame);
this.notify(this._getFooter());
}).catch(() => {
} catch {
this.notify('renderer is busy');
});
}
}
_getFooter() {
@ -277,22 +280,22 @@ class Mapscii {
}
notify(text) {
config.onUpdate && config.onUpdate();
if (!config.headless) {
this.config.onUpdate && this.config.onUpdate();
if (!this.config.headless) {
this._write('\r\x1B[K' + text);
}
}
_write(output) {
config.output.write(output);
this.config.output.write(output);
}
zoomBy(step) {
if (this.zoom+step < this.minZoom) {
return this.zoom = this.minZoom;
}
if (this.zoom+step > config.maxZoom) {
return this.zoom = config.maxZoom;
if (this.zoom+step > this.config.maxZoom) {
return this.zoom = this.config.maxZoom;
}
this.zoom += step;
@ -309,5 +312,3 @@ class Mapscii {
});
}
}
module.exports = Mapscii;

View File

@ -4,19 +4,17 @@
The Console Vector Tile renderer - bäm!
*/
'use strict';
const x256 = require('x256');
const simplify = require('simplify-js');
import x256 from 'x256';
import simplify from 'simplify-js';
const Canvas = require('./Canvas');
const LabelBuffer = require('./LabelBuffer');
const Styler = require('./Styler');
const utils = require('./utils');
const config = require('./config');
import Canvas from './Canvas.js';
import LabelBuffer from './LabelBuffer.js';
import Styler from './Styler.js';
import * as utils from './utils.js';
import config from './config.js';
class Renderer {
constructor(output, tileSource, style) {
this.output = output;
export default class Renderer {
constructor(tileSource, style) {
this.tileSource = tileSource;
this.labelBuffer = new LabelBuffer();
this.styler = new Styler(style);
@ -331,5 +329,3 @@ Renderer.prototype.lastDrawAt = 0;
Renderer.prototype.labelBuffer = null;
Renderer.prototype.tileSource = null;
Renderer.prototype.tilePadding = 64;
module.exports = Renderer;

21
src/Renderer.spec.js Normal file
View File

@ -0,0 +1,21 @@
import fsPromises from 'fs/promises';
import Renderer from './Renderer';
import TileSource from './TileSource.js';
const center = {
lat: 52.51298,
lon: 13.42012,
};
describe('Renderer', () => {
describe('with a HTTP source', () => {
test('does not crash when creating a Renderer', async () => {
const tileSource = new TileSource();
await tileSource.init('http://mapscii.me/');
const style = JSON.parse(await fsPromises.readFile('./styles/dark.json'));
const renderer = new Renderer(tileSource, style);
renderer.setSize(30, 30);
expect(await renderer.draw(center, 13)).toMatchSnapshot();
});
});
});

View File

@ -8,13 +8,11 @@
Compiles layer filter instructions into a chain of true/false returning
anonymous functions to improve rendering speed compared to realtime parsing.
*/
'use strict';
class Styler {
export default class Styler {
constructor(style) {
this.styleById = {};
this.styleByLayer = {};
var base, name;
this.styleName = style.name;
if (style.constants) {
this._replaceConstants(style.constants, style.layers);
@ -31,10 +29,7 @@ class Styler {
layer.appliesTo = this._compileFilter(layer.filter);
//TODO Better translation of: @styleByLayer[style['source-layer']] ?= []
if ((base = this.styleByLayer)[name = layer['source-layer']] == null) {
base[name] = [];
}
this.styleByLayer[layer['source-layer']] ??= [];
this.styleByLayer[layer['source-layer']].push(layer);
this.styleById[layer.id] = layer;
}
@ -129,5 +124,3 @@ class Styler {
}
}
}
module.exports = Styler;

12
src/Styler.spec.js Normal file
View File

@ -0,0 +1,12 @@
import fsPromises from 'fs/promises';
import Styler from './Styler';
describe('Styler', () => {
describe('getStyleFor', () => {
test('returns false for landuse_park, line', async () => {
const style = JSON.parse(await fsPromises.readFile('./styles/dark.json'));
const styler = new Styler(style);
expect(styler.getStyleFor('landuse_park', 'line')).toBe(false);
});
});
});

View File

@ -4,29 +4,24 @@
Handling of and access to single VectorTiles
*/
'use strict';
const VectorTile = require('@mapbox/vector-tile').VectorTile;
const Protobuf = require('pbf');
const zlib = require('zlib');
const RBush = require('rbush');
const x256 = require('x256');
import { VectorTile } from '@mapbox/vector-tile';
import Protobuf from 'pbf';
import zlib from 'zlib';
import RBush from 'rbush';
import x256 from 'x256';
const config = require('./config');
const utils = require('./utils');
import config from './config.js';
import * as utils from './utils.js';
class Tile {
export default class Tile {
constructor(styler) {
this.styler = styler;
}
load(buffer) {
return this._unzipIfNeeded(buffer).then((buffer) => {
return this._loadTile(buffer);
}).then(() => {
return this._loadLayers();
}).then(() => {
return this;
});
async load(buffer) {
const unzippedBuffer = await this._unzipIfNeeded(buffer);
this._loadTile(unzippedBuffer);
this._loadLayers();
}
_loadTile(buffer) {
@ -122,10 +117,10 @@ class Tile {
}
_addBoundaries(deep, data) {
let minX = 2e308;
let maxX = -2e308;
let minY = 2e308;
let maxY = -2e308;
let minX = 2e307;
let maxX = -2e307;
let minY = 2e307;
let maxY = -2e307;
const points = (deep ? data.points[0] : data.points);
for (const p of points) {
if (p.x < minX) minX = p.x;
@ -163,5 +158,3 @@ class Tile {
}
Tile.prototype.layers = {};
module.exports = Tile;

View File

@ -6,15 +6,14 @@
* remote TileServer
* local MBTiles and VectorTiles
*/
'use strict';
const fs = require('fs');
const path = require('path');
const fetch = require('node-fetch');
const envPaths = require('env-paths');
import fsPromises from 'fs/promises';
import path from 'path';
import fetch from 'node-fetch';
import envPaths from 'env-paths';
const paths = envPaths('mapscii');
const Tile = require('./Tile');
const config = require('./config');
import Tile from './Tile.js';
import config from './config.js';
// https://github.com/mapbox/node-mbtiles has native build dependencies (sqlite3)
// To maximize MapSCIIs compatibility, MBTiles support must be manually added via
@ -30,8 +29,8 @@ const modes = {
HTTP: 3,
};
class TileSource {
init(source) {
export default class TileSource {
async init(source) {
this.source = source;
this.cache = {};
@ -44,7 +43,7 @@ class TileSource {
if (this.source.startsWith('http')) {
if (config.persistDownloadedTiles) {
this._initPersistence();
await this._initPersistence();
}
this.mode = modes.HTTP;
@ -102,15 +101,16 @@ class TileSource {
}
}
_getHTTP(z, x, y) {
async _getHTTP(z, x, y) {
let promise;
const persistedTile = this._getPersited(z, x, y);
const persistedTile = await this._getPersited(z, x, y);
if (config.persistDownloadedTiles && persistedTile) {
promise = Promise.resolve(persistedTile);
} else {
promise = fetch(this.source + [z,x,y].join('/') + '.pbf')
.then((res) => res.buffer())
.then((buffer) => {
.then((res) => res.arrayBuffer())
.then((arrayBuffer) => {
const buffer = Buffer.from(arrayBuffer);
if (config.persistDownloadedTiles) {
this._persistTile(z, x, y, buffer);
return buffer;
@ -122,7 +122,7 @@ class TileSource {
});
}
_getMBTile(z, x, y) {
async _getMBTile(z, x, y) {
return new Promise((resolve, reject) => {
this.mbtiles.getTile(z, x, y, (err, buffer) => {
if (err) {
@ -133,40 +133,41 @@ class TileSource {
});
}
_createTile(z, x, y, buffer) {
async _createTile(z, x, y, buffer) {
const name = [z, x, y].join('-');
this.cached.push(name);
const tile = this.cache[name] = new Tile(this.styler);
return tile.load(buffer);
await tile.load(buffer);
return tile;
}
_initPersistence() {
async _initPersistence() {
try {
this._createFolder(paths.cache);
await this._createFolder(paths.cache);
} catch (error) {
config.persistDownloadedTiles = false;
}
}
_persistTile(z, x, y, buffer) {
async _persistTile(z, x, y, buffer) {
const zoom = z.toString();
this._createFolder(path.join(paths.cache, zoom));
await this._createFolder(path.join(paths.cache, zoom));
const filePath = path.join(paths.cache, zoom, `${x}-${y}.pbf`);
return fs.writeFile(filePath, buffer, () => null);
return fsPromises.writeFile(filePath, buffer);
}
_getPersited(z, x, y) {
async _getPersited(z, x, y) {
try {
return fs.readFileSync(path.join(paths.cache, z.toString(), `${x}-${y}.pbf`));
return await fsPromises.readFile(path.join(paths.cache, z.toString(), `${x}-${y}.pbf`));
} catch (error) {
return false;
}
}
_createFolder(path) {
async _createFolder(path) {
try {
fs.mkdirSync(path);
await fsPromises.mkdir(path);
return true;
} catch (error) {
if (error.code === 'EEXIST') return true;
@ -174,5 +175,3 @@ class TileSource {
}
}
}
module.exports = TileSource;

View File

@ -1,5 +1,4 @@
'use strict';
const TileSource = require('./TileSource');
import TileSource from './TileSource.js';
describe('TileSource', () => {
describe('with a HTTP source', () => {

Binary file not shown.

View File

@ -1,4 +1,10 @@
module.exports = {
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default {
language: 'en',
// TODO: adapt to osm2vectortiles successor openmaptiles v3)
@ -7,7 +13,7 @@ module.exports = {
//source: __dirname+"/../mbtiles/regensburg.mbtiles",
styleFile: __dirname+'/../styles/dark.json',
styleFile: __dirname + '/../styles/dark.json',
initialZoom: null,
maxZoom: 18,

View File

@ -4,100 +4,95 @@
methods used all around
*/
'use strict';
const config = require('./config');
import config from './config.js';
const constants = {
RADIUS: 6378137,
};
const utils = {
clamp: (num, min, max) => {
if (num <= min) {
return min;
} else if (num >= max) {
return max;
} else {
return num;
}
},
baseZoom: (zoom) => {
return Math.min(config.tileRange, Math.max(0, Math.floor(zoom)));
},
tilesizeAtZoom: (zoom) => {
return config.projectSize * Math.pow(2, zoom-utils.baseZoom(zoom));
},
deg2rad: (angle) => {
// (angle / 180) * Math.PI
return angle * 0.017453292519943295;
},
ll2tile: (lon, lat, zoom) => {
return {
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),
z: zoom,
};
},
tile2ll: (x, y, zoom) => {
const n = Math.PI - 2*Math.PI*y/Math.pow(2, zoom);
return {
lon: x/Math.pow(2, zoom)*360-180,
lat: 180/Math.PI*Math.atan(0.5*(Math.exp(n)-Math.exp(-n))),
};
},
metersPerPixel: (zoom, lat = 0) => {
return (Math.cos(lat * Math.PI/180) * 2 * Math.PI * constants.RADIUS) / (256 * Math.pow(2, zoom));
},
hex2rgb: (color) => {
if (typeof color !== 'string') return [255, 0, 0];
if (!/^#[a-fA-F0-9]{3,6}$/.test(color)) {
throw new Error(`${color} isn't a supported hex color`);
}
color = color.substr(1);
const decimal = parseInt(color, 16);
if (color.length === 3) {
const rgb = [decimal>>8, (decimal>>4)&15, decimal&15];
return rgb.map((c) => {
return c + (c<<4);
});
} else {
return [(decimal>>16)&255, (decimal>>8)&255, decimal&255];
}
},
digits: (number, digits) => {
return Math.floor(number*Math.pow(10, digits))/Math.pow(10, digits);
},
normalize: (ll) => {
if (ll.lon < -180) ll.lon += 360;
if (ll.lon > 180) ll.lon -= 360;
if (ll.lat > 85.0511) ll.lat = 85.0511;
if (ll.lat < -85.0511) ll.lat = -85.0511;
return ll;
},
population: (val) => {
let bits = 0;
while (val > 0) {
bits += val & 1;
val >>= 1;
}
return bits;
},
export const clamp = (num, min, max) => {
if (num <= min) {
return min;
} else if (num >= max) {
return max;
} else {
return num;
}
};
module.exports = utils;
export const baseZoom = (zoom) => {
return Math.min(config.tileRange, Math.max(0, Math.floor(zoom)));
};
export const tilesizeAtZoom = (zoom) => {
return config.projectSize * Math.pow(2, zoom-baseZoom(zoom));
};
export const deg2rad = (angle) => {
// (angle / 180) * Math.PI
return angle * 0.017453292519943295;
};
export const ll2tile = (lon, lat, zoom) => {
return {
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),
z: zoom,
};
};
export const tile2ll = (x, y, zoom) => {
const n = Math.PI - 2*Math.PI*y/Math.pow(2, zoom);
return {
lon: x/Math.pow(2, zoom)*360-180,
lat: 180/Math.PI*Math.atan(0.5*(Math.exp(n)-Math.exp(-n))),
};
};
export const metersPerPixel = (zoom, lat = 0) => {
return (Math.cos(lat * Math.PI/180) * 2 * Math.PI * constants.RADIUS) / (256 * Math.pow(2, zoom));
};
export const hex2rgb = (color) => {
if (typeof color !== 'string') return [255, 0, 0];
if (!/^#[a-fA-F0-9]{3,6}$/.test(color)) {
throw new Error(`${color} isn't a supported hex color`);
}
color = color.substr(1);
const decimal = parseInt(color, 16);
if (color.length === 3) {
const rgb = [decimal>>8, (decimal>>4)&15, decimal&15];
return rgb.map((c) => {
return c + (c<<4);
});
} else {
return [(decimal>>16)&255, (decimal>>8)&255, decimal&255];
}
};
export const digits = (number, digits) => {
return (Math.floor(number*Math.pow(10, digits))/Math.pow(10, digits)).toFixed(digits);
};
export const normalize = (ll) => {
if (ll.lon < -180) ll.lon += 360;
if (ll.lon > 180) ll.lon -= 360;
if (ll.lat > 85.0511) ll.lat = 85.0511;
if (ll.lat < -85.0511) ll.lat = -85.0511;
return ll;
};
export const population = (val) => {
let bits = 0;
while (val > 0) {
bits += val & 1;
val >>= 1;
}
return bits;
};

View File

@ -1,7 +1,18 @@
'use strict';
const utils = require('./utils');
import * as utils from './utils.js';
describe('utils', () => {
describe('digits', () => {
describe.each([
[1, 0, '1'],
[1, 1, '1.0'],
[3.1415, 3, '3.141'],
])('when given value=%f and digits=%f', (value, digits, expected_value) => {
test(`returns ${expected_value}`, () => {
expect(utils.digits(value, digits)).toEqual(expected_value);
});
});
});
describe('hex2rgb', () => {
describe.each([
['#ff0000', 255, 0, 0],
@ -22,26 +33,26 @@ describe('utils', () => {
expect(wrapper).toThrow('isn\'t a supported hex color');
});
});
});
describe('normalize', () => {
describe.each([
[0, 0, 0, 0],
[61, 48, 61, 48],
[-61, -48, -61, -48],
[181, 85.06, -179, 85.0511],
[-181, -85.06, 179, -85.0511],
])('when given lon=%f and lat=%f', (lon, lat, expected_lon, expected_lat) => {
const input = {
lon,
lat,
};
test(`returns lon=${expected_lon} and lat=${expected_lat}`, () => {
const expected = {
lon: expected_lon,
lat: expected_lat,
describe('normalize', () => {
describe.each([
[0, 0, 0, 0],
[61, 48, 61, 48],
[-61, -48, -61, -48],
[181, 85.06, -179, 85.0511],
[-181, -85.06, 179, -85.0511],
])('when given lon=%f and lat=%f', (lon, lat, expected_lon, expected_lat) => {
const input = {
lon,
lat,
};
expect(utils.normalize(input)).toEqual(expected);
test(`returns lon=${expected_lon} and lat=${expected_lat}`, () => {
const expected = {
lon: expected_lon,
lat: expected_lat,
};
expect(utils.normalize(input)).toEqual(expected);
});
});
});
});