Repository: rastapasta/mapscii Branch: master Commit: 4fe9a60a0c9d Files: 29 Total size: 123.9 KB Directory structure: gitextract_o17myi0p/ ├── .eslintrc.js ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── BugReport.md │ ├── FeatureRequest.md │ ├── Question.md │ └── Support.md ├── .gitignore ├── .npmignore ├── .travis.yml ├── AUTHORS ├── LICENSE ├── README.md ├── bin/ │ └── mapscii.sh ├── main.js ├── package.json ├── snap/ │ └── snapcraft.yaml ├── src/ │ ├── BrailleBuffer.js │ ├── Canvas.js │ ├── LabelBuffer.js │ ├── Mapscii.js │ ├── Renderer.js │ ├── Styler.js │ ├── Tile.js │ ├── TileSource.js │ ├── TileSource.spec.js │ ├── config.js │ ├── utils.js │ └── utils.spec.js └── styles/ ├── bright.json └── dark.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.js ================================================ module.exports = { "env": { "es6": true, "node": true, "jest": true }, "parserOptions": { "ecmaVersion": 2018 }, "extends": [ "eslint:recommended", "plugin:jest/recommended" ], "rules": { "indent": [ "error", 2, { "SwitchCase": 1 } ], "linebreak-style": [ "error", "unix" ], "no-console": 0, "quotes": [ "error", "single" ], "semi": [ "error", "always" ] } }; ================================================ FILE: .github/ISSUE_TEMPLATE/BugReport.md ================================================ --- name: "\U0001F41B Bug report" about: Something isn’t working as expected --- **Steps to reproduce** **Current behavior** **Expected behavior** ================================================ FILE: .github/ISSUE_TEMPLATE/FeatureRequest.md ================================================ --- name: "\U0001F680 Feature request" about: I have a suggestion (and might want to implement myself) --- **The problem** **Proposed solution** **Alternative solutions** **Additional context** ================================================ FILE: .github/ISSUE_TEMPLATE/Question.md ================================================ --- name: "\U0001F914 Support question" about: I have a question or don’t know how to do something --- --------------^ Click “Preview”! If you have a support question, feel free to ask it here. Before submitting a new question, make sure you: - Searched opened and closed [GitHub issues](https://github.com/rastapasta/mapscii/issues?utf8=%E2%9C%93&q=is%3Aissue). - Read [the introduction](https://github.com/rastapasta/mapscii/blob/master/README.md). - Read [the wiki article](hhttps://wiki.openstreetmap.org/wiki/Mapscii). ================================================ FILE: .github/ISSUE_TEMPLATE/Support.md ================================================ --- name: "\U0001F984 Support MapSCII’s development" about: I want to support efforts in maintaining this community-driven project --- --------------^ Click “Preview”! Developing and maintaining an open source project is a big effort. MapSCII isn’t supported by any big company, and all the contributors are working on it in their free time. We need your help to make it sustainable. There are many ways you can help: - Answer questions in [GitHub issues](https://github.com/rastapasta/mapscii/issues). - Review [pull requests](https://github.com/rastapasta/mapscii/pulls). - Fix bugs and add new features. - Write articles and talk about MapSCII on conferences and meetups (we’re always happy to review your texts and slides). ================================================ FILE: .gitignore ================================================ node_modules bundle* *.log tmp mbtiles tiles .DS_Store ================================================ FILE: .npmignore ================================================ node_modules bundle* *.log tmp mbtiles tiles .DS_Store docs ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - "14" - "16" - "18" script: - npm run-script lint - npm test ================================================ FILE: AUTHORS ================================================ # This is the list of MapSCII authors for copyright purposes. # Michael Straßburger Christian Paul (https://chrpaul.de) Jannis R Alexander Zhukov (https://github.com/ZhukovAlexander) Quincy Morgan (https://github.com/quincylvania) lennonhill (https://github.com/lennonhill) ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 Michael Straßburger Copyright (c) 2019 The MapSCII authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # MapSCII - The Whole World In Your Console. [![Build Status](https://travis-ci.com/rastapasta/mapscii.svg?branch=master)](https://travis-ci.com/rastapasta/mapscii) 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. ![asciicast](https://cloud.githubusercontent.com/assets/1259904/25480718/497a64e2-2b4a-11e7-9cf0-ed52ee0b89c0.png) ## Try it out! ```sh $ telnet mapscii.me ``` If you're on Windows, use the open source telnet client [PuTTY](https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html) to connect. ## Features * Use your mouse to drag and zoom in and out! * Discover Point-of-Interests around any given location * Highly customizable layer styling with [Mapbox Styles](https://www.mapbox.com/mapbox-gl-style-spec/) support * Connect to any public or private vector tile server * Or just use the supplied and optimized [OSM2VectorTiles](https://github.com/osm2vectortiles) based one * 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 * Highly optimized algorithms for a smooth experience * 100% pure JavaScript! :sunglasses: ## How to run it locally With a modern node installation available, just start it with ``` npx mapscii ``` ## How to install it locally ### With npm If you haven't already got Node.js >= version 10, then [go get it](http://nodejs.org/). ``` npm install -g mapscii ``` If you're on OSX, or get an error about file permissions, you may need to do ```sudo npm install -g mapscii``` ### With snap In any of the [supported Linux distros](https://snapcraft.io/docs/core/install): sudo snap install mapscii (This snap is maintained by [@nathanhaines](https://github.com/nathanhaines/)) ## Running This is pretty simple too. ``` mapscii ``` ## Keyboard shortcuts * Arrows **up**, **down**, **left**, **right** to scroll around * Press **a** or **z** to zoom in and out * Press **c** to switch to block character mode * Press **q** to quit ## Mouse control If your terminal supports mouse events you can drag the map and use your scroll wheel to zoom in and out. ## Behind the scenes ### Libraries #### Mastering the console * [`x256`](https://github.com/substack/node-x256) for converting RGB values to closest xterm-256 [color code](https://en.wikipedia.org/wiki/File:Xterm_256color_chart.svg) * [`term-mouse`](https://github.com/CoderPuppy/term-mouse) for mouse handling * [`keypress`](https://github.com/TooTallNate/keypress) for input handling * [`string-width`](https://github.com/sindresorhus/string-width) to determine visual string lengths #### Discovering the map data * [`vector-tile`](https://github.com/mapbox/vector-tile-js) for [VectorTile](https://github.com/mapbox/vector-tile-spec/tree/master/2.1) parsing * [`pbf`](https://github.com/mapbox/pbf) for [Protobuf](https://developers.google.com/protocol-buffers/) decoding * [`mbtiles`](https://github.com/mapbox/node-mbtiles) for [MBTiles](https://github.com/mapbox/mbtiles-spec/blob/master/1.2/spec.md) parsing #### Juggling the vectors and numbers * [`earcut`](https://github.com/mapbox/earcut) for polygon triangulation * [`rbush`](https://github.com/mourner/rbush) for 2D spatial indexing of geo and label data * [`bresenham`](https://github.com/madbence/node-bresenham) for line point calculations * [`simplify-js`](https://github.com/mourner/simplify-js) for polyline simplifications #### Handling the flow * [`node-fetch`](https://github.com/bitinn/node-fetch) for HTTP requests * [`env-paths`](https://github.com/sindresorhus/env-paths) to determine where to persist downloaded tiles ### TODOs * MapSCII * [ ] GeoJSON support via [geojson-vt](https://github.com/mapbox/geojson-vt) * [ ] CLI support * [-] startup parameters * [X] TileSource * [X] Style * [X] center position * [X] zoom * [ ] demo mode? * [ ] mouse control * [ ] hover POIs/labels * [ ] hover maybe even polygons/-lines? * Styler * [ ] respect zoom based style ranges * Renderer * [ ] download and process tiles in a different thread ([#3](https://github.com/rastapasta/mapscii/issues/3)) * [ ] optimize renderer for large areas ([#6](https://github.com/rastapasta/mapscii/issues/6)) * [ ] label drawing * [ ] multi line label? * TileSource * [ ] implement single vector-tile handling ## Special thanks * [lukasmartinelli](https://github.com/lukasmartinelli) & [manuelroth](https://github.com/manuelroth) for all their work on [OSM2VectorTiles](https://github.com/osm2vectortiles) (global vector tiles from [OSM Planet](https://wiki.openstreetmap.org/wiki/Planet.osm)) * [mourner](https://github.com/mourner) for all his work on mindblowing GIS algorithms (like the used [earcut](https://github.com/mapbox/earcut), [rbush](https://github.com/mourner/rbush), [simplify-js](https://github.com/mourner/simplify-js), ..) ## Licenses ### Map data #### The Open Data Commons Open Database License (oDbl) [OpenStreetMap](https://www.openstreetmap.org) is open data, licensed under the [Open Data Commons Open Database License](http://opendatacommons.org/licenses/odbl/) (ODbL) by the [OpenStreetMap Foundation](http://osmfoundation.org/) (OSMF). You are free to copy, distribute, transmit and adapt our data, as long as you credit OpenStreetMap and its contributors. If you alter or build upon our data, you may distribute the result only under the same licence. The full [legal code](http://opendatacommons.org/licenses/odbl/1.0/) explains your rights and responsibilities. The cartography in our map tiles, and our documentation, are licenced under the [Creative Commons Attribution-ShareAlike 2.0](http://creativecommons.org/licenses/by-sa/2.0/) licence (CC BY-SA). ### MapSCII * [License](./LICENSE) * [Authors](./AUTHORS) ================================================ FILE: bin/mapscii.sh ================================================ #!/bin/sh ':' //; # Based on https://github.com/MrRio/vtop/blob/master/bin/vtop.js ':' //; export TERM=xterm-256color ':' //; exec "$(command -v node || command -v nodejs)" "$0" "$@" 'use strict' require('../main.js'); ================================================ FILE: main.js ================================================ /*# MapSCII - Terminal Map Viewer by Michael Strassburger Discover the planet in your console! This scripts boots up the application. TODO: params parsing and so on #*/ 'use strict'; const config = require('./src/config'); const Mapscii = require('./src/Mapscii'); const argv = require('yargs') .option('latitude', { alias: 'lat', description: 'Latitude of initial centre', default: config.initialLat, type: 'number', }) .option('longitude', { alias: 'lon', description: 'Longitude of initial centre', default: config.initialLon, type: 'number', }) .option('zoom', { alias: 'z', description: 'Initial zoom', default: config.initialZoom, type: 'number', }) .option('width', { alias: 'w', description: 'Fixed width of rendering', type: 'number', }) .option('height', { alias: 'h', description: 'Fixed height of rendering', type: 'number', }) .option('braille', { alias: 'b', description: 'Activate braille rendering', default: config.useBraille, type: 'boolean', }) .option('headless', { alias: 'H', description: 'Activate headless mode', default: config.headless, type: 'boolean', }) .option('tile_source', { alias: 'tileSource', description: 'URL or path to osm2vectortiles source', default: config.source, type: 'string', }) .option('style_file', { alias: 'style', description: 'path to json style file', default: config.styleFile, type: 'string', }) .strict() .argv; const options = { initialLat: argv.latitude, initialLon: argv.longitude, initialZoom: argv.zoom, size: { width: argv.width, height: argv.height }, useBraille: argv.braille, headless: argv.headless, source: argv.tile_source, styleFile: argv.style_file, }; const mapscii = new Mapscii(options); mapscii.init().catch((err) => { console.error('Failed to start MapSCII.'); console.error(err); }); ================================================ FILE: package.json ================================================ { "name": "mapscii", "version": "0.3.1", "description": "MapSCII is a Braille & ASCII world map renderer for your console, based on OpenStreetMap", "main": "main.js", "scripts": { "lint": "eslint src", "start": "node main", "test": "jest" }, "repository": { "type": "git", "url": "https://github.com/rastapasta/mapscii.git" }, "bin": { "mapscii": "./bin/mapscii.sh" }, "engines": { "node": ">=10" }, "keywords": [ "map", "console", "terminal", "ascii", "osm", "vectortile", "mbtiles" ], "author": "Michael Straßburger ", "license": "MIT", "dependencies": { "@mapbox/vector-tile": "^1.3.1", "bresenham": "0.0.4", "earcut": "^2.2.2", "env-paths": "^2.2.0", "keypress": "^0.2.1", "node-fetch": "^2.6.1", "pbf": "^3.2.1", "rbush": "^3.0.1", "simplify-js": "^1.2.4", "string-width": "^4.2.0", "term-mouse": "^0.2.2", "x256": "0.0.2", "yargs": "^15.4.1" }, "devDependencies": { "eslint": "^7.8.1", "eslint-plugin-jest": "^24.0.0", "jest": "^26.4.2" } } ================================================ FILE: snap/snapcraft.yaml ================================================ name: mapscii version: master summary: The Whole World In Your Console description: | A node.js based Vector Tile to Braille and ASCII renderer for xterm-compatible terminals. grade: devel # must be 'stable' to release into candidate/stable channels confinement: strict apps: mapscii: command: mapscii plugs: [network] parts: mapscii: source: . plugin: nodejs ================================================ FILE: src/BrailleBuffer.js ================================================ /* termap - Terminal Map Viewer by Michael Strassburger Simple pixel to braille character mapper Implementation inspired by node-drawille (https://github.com/madbence/node-drawille) * added color support * added text label support * general optimizations 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'); const asciiMap = { // '▬': [2+32, 4+64], // '¯': [1+16], '▀': [1+2+16+32], '▄': [4+8+64+128], '■': [2+4+32+64], '▌': [1+2+4+8], '▐': [16+32+64+128], // '▓': [1+4+32+128, 2+8+16+64], '█': [255], }; const termReset = '\x1B[39;49m'; class BrailleBuffer { constructor(width, height) { this.brailleMap = [[0x1, 0x8],[0x2, 0x10],[0x4, 0x20],[0x40, 0x80]]; this.pixelBuffer = null; this.charBuffer = null; this.foregroundBuffer = null; this.backgroundBuffer = null; this.asciiToBraille = []; this.globalBackground = null; this.width = width; this.height = height; const size = width*height/8; this.pixelBuffer = Buffer.alloc(size); this.foregroundBuffer = Buffer.alloc(size); this.backgroundBuffer = Buffer.alloc(size); this._mapBraille(); this.clear(); } clear() { this.pixelBuffer.fill(0); this.charBuffer = []; this.foregroundBuffer.fill(0); this.backgroundBuffer.fill(0); } setGlobalBackground(background) { this.globalBackground = background; } setBackground(x, y, color) { if (0 <= x && x < this.width && 0 <= y && y < this.height) { const idx = this._project(x, y); this.backgroundBuffer[idx] = color; } } setPixel(x, y, color) { this._locate(x, y, (idx, mask) => { this.pixelBuffer[idx] |= mask; this.foregroundBuffer[idx] = color; }); } unsetPixel(x, y) { this._locate(x, y, (idx, mask) => { this.pixelBuffer[idx] &= ~mask; }); } _project(x, y) { return (x>>1) + (this.width>>1)*(y>>2); } _locate(x, y, cb) { if (!((0 <= x && x < this.width) && (0 <= y && y < this.height))) { return; } const idx = this._project(x, y); const mask = this.brailleMap[y & 3][x & 1]; return cb(idx, mask); } _mapBraille() { this.asciiToBraille = [' ']; const masks = []; for (const char in asciiMap) { const bits = asciiMap[char]; if (!(bits instanceof Array)) continue; for (const mask of bits) { masks.push({ mask: mask, char: char, }); } } //TODO Optimize this part var i, k; const results = []; for (i = k = 1; k <= 255; i = ++k) { 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); if (!best || best.covered < covered) { return { char: mask.char, covered: covered, }; } else { return best; } }), void 0).char); } return results; } _termColor(foreground, background) { background |= this.globalBackground; if (foreground && background) { return `\x1B[38;5;${foreground};48;5;${background}m`; } else if (foreground) { return `\x1B[49;38;5;${foreground}m`; } else if (background) { return `\x1B[39;48;5;${background}m`; } else { return termReset; } } frame() { const output = []; let currentColor = null; let skip = 0; for (let y = 0; y < this.height/4; y++) { skip = 0; for (let x = 0; x < this.width/2; x++) { const idx = y*this.width/2 + x; if (idx && !x) { output.push(config.delimeter); } const colorCode = this._termColor(this.foregroundBuffer[idx], this.backgroundBuffer[idx]); if (currentColor !== colorCode) { output.push(currentColor = colorCode); } const char = this.charBuffer[idx]; if (char) { skip += stringWidth(char)-1; if (skip+x < this.width/2) { output.push(char); } } else { if (!skip) { if (config.useBraille) { output.push(String.fromCharCode(0x2800+this.pixelBuffer[idx])); } else { output.push(this.asciiToBraille[this.pixelBuffer[idx]]); } } else { skip--; } } } } output.push(termReset+config.delimeter); return output.join(''); } setChar(char, x, y, color) { if (0 <= x && x < this.width && 0 <= y && y < this.height) { const idx = this._project(x, y); this.charBuffer[idx] = char; this.foregroundBuffer[idx] = color; } } writeText(text, x, y, color, center = true) { if (center) { x -= text.length/2+1; } for (let i = 0; i < text.length; i++) { this.setChar(text.charAt(i), x+i*2, y, color); } } } module.exports = BrailleBuffer; ================================================ FILE: src/Canvas.js ================================================ /* termap - Terminal Map Viewer by Michael Strassburger Canvas-like painting abstraction for BrailleBuffer Implementation inspired by node-drawille-canvas (https://github.com/madbence/node-drawille-canvas) * added support for filled polygons * improved text rendering 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'); class Canvas { constructor(width, height) { this.width = width; this.height = height; this.buffer = new BrailleBuffer(width, height); } frame() { return this.buffer.frame(); } clear() { this.buffer.clear(); } text(text, x, y, color, center = false) { this.buffer.writeText(text, x, y, color, center); } line(from, to, color, width = 1) { this._line(from.x, from.y, to.x, to.y, color, width); } polyline(points, color, width = 1) { for (let i = 1; i < points.length; i++) { const x1 = points[i - 1].x; const y1 = points[i - 1].y; this._line(x1, y1, points[i].x, points[i].y, width, color); } } setBackground(color) { this.buffer.setGlobalBackground(color); } background(x, y, color) { this.buffer.setBackground(x, y, color); } polygon(rings, color) { const vertices = []; const holes = []; for (const ring of rings) { if (vertices.length) { if (ring.length < 3) continue; holes.push(vertices.length / 2); } else { if (ring.length < 3) return false; } for (const point of ring) { vertices.push(point.x); vertices.push(point.y); } } let triangles; try { triangles = earcut(vertices, holes); } catch (error) { return false; } for (let i = 0; i < triangles.length; i += 3) { const pa = this._polygonExtract(vertices, triangles[i]); const pb = this._polygonExtract(vertices, triangles[i + 1]); const pc = this._polygonExtract(vertices, triangles[i + 2]); this._filledTriangle(pa, pb, pc, color); } return true; } _polygonExtract(vertices, pointId) { return [vertices[pointId * 2], vertices[pointId * 2 + 1]]; } // Inspired by Alois Zingl's "The Beauty of Bresenham's Algorithm" // -> http://members.chello.at/~easyfilter/bresenham.html _line(x0, y0, x1, y1, width, color) { // Fall back to width-less bresenham algorithm if we dont have a width if (!(width = Math.max(0, width - 1))) { return bresenham(x0, y0, x1, y1, (x, y) => { return this.buffer.setPixel(x, y, color); }); } const dx = Math.abs(x1 - x0); const sx = x0 < x1 ? 1 : -1; const dy = Math.abs(y1 - y0); const sy = y0 < y1 ? 1 : -1; let err = dx - dy; const ed = dx + dy === 0 ? 1 : Math.sqrt(dx * dx + dy * dy); width = (width + 1) / 2; /* eslint-disable no-constant-condition */ while (true) { this.buffer.setPixel(x0, y0, color); let e2 = err; let x2 = x0; if (2 * e2 >= -dx) { e2 += dy; let y2 = y0; while (e2 < ed * width && (y1 !== y2 || dx > dy)) { this.buffer.setPixel(x0, y2 += sy, color); e2 += dx; } if (x0 === x1) { break; } e2 = err; err -= dy; x0 += sx; } if (2 * e2 <= dy) { e2 = dx - e2; while (e2 < ed * width && (x1 !== x2 || dx < dy)) { this.buffer.setPixel(x2 += sx, y0, color); e2 += dy; } if (y0 === y1) { break; } err += dx; y0 += sy; } } /* eslint-enable */ } _filledRectangle(x, y, width, height, color) { const pointA = [x, y]; const pointB = [x + width, y]; const pointC = [x, y + height]; const pointD = [x + width, y + height]; this._filledTriangle(pointA, pointB, pointC, color); this._filledTriangle(pointC, pointB, pointD, color); } _bresenham(pointA, pointB) { return bresenham(pointA[0], pointA[1], pointB[0], pointB[1]); } // Draws a filled triangle _filledTriangle(pointA, pointB, pointC, color) { const a = this._bresenham(pointB, pointC); 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); }).sort(function(a, b) { if (a.y === b.y) { return a.x - b.x; } else { return a.y - b.y; } }); for (let i = 0; i < points.length; i++) { const point = points[i]; const next = points[i * 1 + 1]; if (point.y === (next || {}).y) { const left = Math.max(0, point.x); const right = Math.min(this.width - 1, next.x); if (left >= 0 && right <= this.width) { for (let x = left; x <= right; x++) { this.buffer.setPixel(x, point.y, color); } } } else { this.buffer.setPixel(point.x, point.y, color); } if (!next) { break; } } } } Canvas.prototype.stack = []; module.exports = Canvas; ================================================ FILE: src/LabelBuffer.js ================================================ /* termap - Terminal Map Viewer by Michael Strassburger 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, }; } }; ================================================ FILE: src/Mapscii.js ================================================ /* MapSCII - Terminal Map Viewer by Michael Strassburger UI and central command center */ 'use strict'; const fs = require('fs'); const keypress = require('keypress'); const TermMouse = require('term-mouse'); const Renderer = require('./Renderer'); const TileSource = require('./TileSource'); const utils = require('./utils'); let config = require('./config'); class Mapscii { constructor(options) { this.width = null; this.height = null; this.canvas = null; this.mouse = null; this.mouseDragging = false; this.mousePosition = { x: 0, y: 0, }; this.tileSource = null; this.renderer = null; this.zoom = 0; this.minZoom = null; config = Object.assign(config, options); this.center = { lat: config.initialLat, lon: config.initialLon }; } async init() { if (!config.headless) { this._initKeyboard(); this._initMouse(); } this._initTileSource(); this._initRenderer(); this._draw(); this.notify('Welcome to MapSCII! Use your cursors to navigate, a/z to zoom, q to quit.'); } _initTileSource() { this.tileSource = new TileSource(); this.tileSource.init(config.source); } _initKeyboard() { keypress(config.input); if (config.input.setRawMode) { config.input.setRawMode(true); } config.input.resume(); config.input.on('keypress', (ch, key) => this._onKey(key)); } _initMouse() { this.mouse = TermMouse({ input: config.input, output: config.output, }); this.mouse.start(); this.mouse.on('click', (event) => this._onClick(event)); this.mouse.on('scroll', (event) => this._onMouseScroll(event)); 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); config.output.on('resize', () => { this._resizeRenderer(); this._draw(); }); this._resizeRenderer(); this.zoom = (config.initialZoom !== null) ? 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.minZoom = 4-Math.log(4096/this.width)/Math.LN2; this.renderer.setSize(this.width, this.height); } _colrow2ll(x, y) { const projected = { x: (x-0.5)*2, y: (y-0.5)*4, }; const size = utils.tilesizeAtZoom(this.zoom); const [dx, dy] = [projected.x-this.width/2, projected.y-this.height/2]; const z = utils.baseZoom(this.zoom); 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)); } _updateMousePosition(event) { this.mousePosition = this._colrow2ll(event.x, event.y); } _onClick(event) { if (event.x < 0 || event.x > this.width/2 || event.y < 0 || event.y > this.height/4) { return; } this._updateMousePosition(event); if (this.mouseDragging && event.button === 'left') { this.mouseDragging = false; } else { this.setCenter(this.mousePosition.lat, this.mousePosition.lon); } this._draw(); } _onMouseScroll(event) { this._updateMousePosition(event); // the location of the pointer, where we want to zoom toward const targetMouseLonLat = this._colrow2ll(event.x, event.y); // zoom toward the center this.zoomBy(config.zoomStep * (event.button === 'up' ? 1 : -1)); // the location the pointer ended up after zooming const offsetMouseLonLat = this._colrow2ll(event.x, event.y); const z = utils.baseZoom(this.zoom); // the projected locations const targetMouseTile = utils.ll2tile(targetMouseLonLat.lon, targetMouseLonLat.lat, z); const offsetMouseTile = utils.ll2tile(offsetMouseLonLat.lon, offsetMouseLonLat.lat, z); // the projected center const centerTile = utils.ll2tile(this.center.lon, this.center.lat, z); // calculate a new center that puts the pointer back in the target location const offsetCenterLonLat = utils.tile2ll( centerTile.x - (offsetMouseTile.x - targetMouseTile.x), centerTile.y - (offsetMouseTile.y - targetMouseTile.y), z ); // move to the new center this.setCenter(offsetCenterLonLat.lat, offsetCenterLonLat.lon); this._draw(); } _onMouseMove(event) { if (event.x < 0 || event.x > this.width/2 || event.y < 0 || event.y > this.height/4) { return; } if (config.mouseCallback && !config.mouseCallback(event)) { return; } // start dragging if (event.button === 'left') { if (this.mouseDragging) { const dx = (this.mouseDragging.x-event.x)*2; const dy = (this.mouseDragging.y-event.y)*4; const size = utils.tilesizeAtZoom(this.zoom); const newCenter = utils.tile2ll( this.mouseDragging.center.x+(dx/size), this.mouseDragging.center.y+(dy/size), utils.baseZoom(this.zoom) ); this.setCenter(newCenter.lat, newCenter.lon); this._draw(); } else { this.mouseDragging = { x: event.x, y: event.y, center: utils.ll2tile(this.center.lon, this.center.lat, utils.baseZoom(this.zoom)), }; } } this._updateMousePosition(event); this.notify(this._getFooter()); } _onKey(key) { if (config.keyCallback && !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(); } else { process.exit(0); } break; case 'a': this.zoomBy(config.zoomStep); break; case 'y': case 'z': this.zoomBy(-config.zoomStep); break; case 'left': case 'h': this.moveBy(0, -8/Math.pow(2, this.zoom)); break; case 'right': case 'l': this.moveBy(0, 8/Math.pow(2, this.zoom)); break; case 'up': case 'k': this.moveBy(6/Math.pow(2, this.zoom), 0); break; case 'down': case 'j': this.moveBy(-6/Math.pow(2, this.zoom), 0); break; case 'c': config.useBraille = !config.useBraille; break; default: draw = false; } if (draw) { this._draw(); } } _draw() { this.renderer.draw(this.center, this.zoom).then((frame) => { this._write(frame); this.notify(this._getFooter()); }).catch(() => { this.notify('renderer is busy'); }); } _getFooter() { // tile = utils.ll2tile(this.center.lon, this.center.lat, this.zoom); // `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)} `; footer += ` zoom: ${utils.digits(this.zoom, 2)} `; if (this.mousePosition.lat !== undefined) { footer += ` mouse: ${utils.digits(this.mousePosition.lat, 3)}, ${utils.digits(this.mousePosition.lon, 3)} `; } return footer; } notify(text) { config.onUpdate && config.onUpdate(); if (!config.headless) { this._write('\r\x1B[K' + text); } } _write(output) { 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; } this.zoom += step; } moveBy(lat, lon) { this.setCenter(this.center.lat+lat, this.center.lon+lon); } setCenter(lat, lon) { this.center = utils.normalize({ lon: lon, lat: lat, }); } } module.exports = Mapscii; ================================================ FILE: src/Renderer.js ================================================ /* termap - Terminal Map Viewer by Michael Strassburger The Console Vector Tile renderer - bäm! */ 'use strict'; const x256 = require('x256'); const simplify = require('simplify-js'); const Canvas = require('./Canvas'); const LabelBuffer = require('./LabelBuffer'); const Styler = require('./Styler'); const utils = require('./utils'); const config = require('./config'); class Renderer { constructor(output, tileSource, style) { this.output = output; this.tileSource = tileSource; this.labelBuffer = new LabelBuffer(); this.styler = new Styler(style); this.tileSource.useStyler(this.styler); } setSize(width, height) { this.width = width; this.height = height; this.canvas = new Canvas(width, height); } async draw(center, zoom) { if (this.isDrawing) return Promise.reject(); this.isDrawing = true; this.labelBuffer.clear(); this._seen = {}; let ref; const color = ((ref = this.styler.styleById['background']) !== null ? ref.paint['background-color'] : void 0 ); if (color) { this.canvas.setBackground(x256(utils.hex2rgb(color))); } this.canvas.clear(); try { let tiles = this._visibleTiles(center, zoom); await Promise.all(tiles.map(async(tile) => { await this._getTile(tile); this._getTileFeatures(tile, zoom); })); await this._renderTiles(tiles); return this._getFrame(); } catch(e) { console.error(e); } finally { this.isDrawing = false; this.lastDrawAt = Date.now(); } } _visibleTiles(center, zoom) { const z = utils.baseZoom(zoom); center = utils.ll2tile(center.lon, center.lat, z); const tiles = []; const tileSize = utils.tilesizeAtZoom(zoom); for (let y = Math.floor(center.y) - 1; y <= Math.floor(center.y) + 1; y++) { for (let x = Math.floor(center.x) - 1; x <= Math.floor(center.x) + 1; x++) { const tile = {x, y, z}; const position = { x: this.width / 2 - (center.x - tile.x) * tileSize, y: this.height / 2 - (center.y - tile.y) * tileSize, }; const gridSize = Math.pow(2, z); tile.x %= gridSize; if (tile.x < 0) { tile.x = z === 0 ? 0 : tile.x + gridSize; } if (tile.y < 0 || tile.y >= gridSize || position.x + tileSize < 0 || position.y + tileSize < 0 || position.x > this.width || position.y > this.height) { continue; } tiles.push({ xyz: tile, zoom, position, size: tileSize, }); } } return tiles; } async _getTile(tile) { tile.data = await this.tileSource.getTile(tile.xyz.z, tile.xyz.x, tile.xyz.y); return tile; } _getTileFeatures(tile, zoom) { const position = tile.position; const layers = {}; const drawOrder = this._generateDrawOrder(zoom); for (const layerId of drawOrder) { const layer = (tile.data.layers || {})[layerId]; if (!layer) { continue; } const scale = layer.extent / utils.tilesizeAtZoom(zoom); layers[layerId] = { scale: scale, features: layer.tree.search({ minX: -position.x * scale, minY: -position.y * scale, maxX: (this.width - position.x) * scale, maxY: (this.height - position.y) * scale }), }; } tile.layers = layers; return tile; } _renderTiles(tiles) { const labels = []; if (tiles.length === 0) return; const drawOrder = this._generateDrawOrder(tiles[0].xyz.z); for (const layerId of drawOrder) { for (const tile of tiles) { const layer = tile.layers[layerId]; if (!layer) continue; for (const feature of layer.features) { // continue if feature.id and drawn[feature.id] // drawn[feature.id] = true; if (layerId.match(/label/)) { labels.push({ tile, feature, scale: layer.scale }); } else { this._drawFeature(tile, feature, layer.scale); } } } } labels.sort((a, b) => { return a.feature.sorty - b.feature.sort; }); for (const label of labels) { this._drawFeature(label.tile, label.feature, label.scale); } } _getFrame() { let frame = ''; if (!this.lastDrawAt) { frame += this.terminal.CLEAR; } frame += this.terminal.MOVE; frame += this.canvas.frame(); return frame; } featuresAt(x, y) { return this.labelBuffer.featuresAt(x, y); } _drawFeature(tile, feature, scale) { let points, placed; if (feature.style.minzoom && tile.zoom < feature.style.minzoom) { return false; } else if (feature.style.maxzoom && tile.zoom > feature.style.maxzoom) { return false; } switch (feature.style.type) { case 'line': { let width = feature.style.paint['line-width']; if (width instanceof Object) { // TODO: apply the correct zoom based value width = width.stops[0][1]; } points = this._scaleAndReduce(tile, feature, feature.points, scale); if (points.length) { this.canvas.polyline(points, feature.color, width); } break; } case 'fill': { points = feature.points.map((p) => { return this._scaleAndReduce(tile, feature, p, scale, false); }); this.canvas.polygon(points, feature.color); break; } case 'symbol': { const genericSymbol = config.poiMarker; const text = feature.label || config.poiMarker; if (this._seen[text] && !genericSymbol) { return false; } placed = false; const pointsOfInterest = this._scaleAndReduce(tile, feature, feature.points, scale); for (const point of pointsOfInterest) { const x = point.x - text.length; const layerMargin = (config.layers[feature.layer] || {}).margin; const margin = layerMargin || config.labelMargin; if (this.labelBuffer.writeIfPossible(text, x, point.y, feature, margin)) { this.canvas.text(text, x, point.y, feature.color); placed = true; break; } else { const cluster = (config.layers[feature.layer] || {}).cluster; if (cluster && this.labelBuffer.writeIfPossible(config.poiMarker, point.x, point.y, feature, 3)) { this.canvas.text(config.poiMarker, point.x, point.y, feature.color); placed = true; break; } } } if (placed) { this._seen[text] = true; } break; } } return true; } _scaleAndReduce(tile, feature, points, scale, filter = true) { let lastX; let lastY; let outside; const scaled = []; const minX = -this.tilePadding; const minY = -this.tilePadding; const maxX = this.width + this.tilePadding; const maxY = this.height + this.tilePadding; for (const point of points) { const x = Math.floor(tile.position.x + (point.x / scale)); const y = Math.floor(tile.position.y + (point.y / scale)); if (lastX === x && lastY === y) { continue; } lastY = y; lastX = x; if (filter) { if (x < minX || x > maxX || y < minY || y > maxY) { if (outside) { continue; } outside = true; } else { if (outside) { outside = null; scaled.push({x: lastX, y: lastY}); } } } scaled.push({x, y}); } if (feature.style.type !== 'symbol') { if (scaled.length < 2) { return []; } if (config.simplifyPolylines) { return simplify(scaled, .5, true); } else { return scaled; } } else { return scaled; } } _generateDrawOrder(zoom) { if (zoom < 2) { return [ 'admin', 'water', 'country_label', 'marine_label', ]; } else { return [ 'landuse', 'water', 'marine_label', 'building', 'road', 'admin', 'country_label', 'state_label', 'water_label', 'place_label', 'rail_station_label', 'poi_label', 'road_label', 'housenum_label', ]; } } } Renderer.prototype.terminal = { CLEAR: '\x1B[2J', MOVE: '\x1B[?6h', }; Renderer.prototype.isDrawing = false; Renderer.prototype.lastDrawAt = 0; Renderer.prototype.labelBuffer = null; Renderer.prototype.tileSource = null; Renderer.prototype.tilePadding = 64; module.exports = Renderer; ================================================ FILE: src/Styler.js ================================================ /* termap - Terminal Map Viewer by Michael Strassburger Minimalistic parser and compiler for Mapbox (Studio) Map Style files See: https://www.mapbox.com/mapbox-gl-style-spec/ 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 { constructor(style) { this.styleById = {}; this.styleByLayer = {}; var base, name; this.styleName = style.name; if (style.constants) { this._replaceConstants(style.constants, style.layers); } for (const layer of style.layers) { if (layer.ref && this.styleById[layer.ref]) { for (const ref of ['type', 'source-layer', 'minzoom', 'maxzoom', 'filter']) { if (this.styleById[layer.ref][ref] && !layer[ref]) { layer[ref] = this.styleById[layer.ref][ref]; } } } 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']].push(layer); this.styleById[layer.id] = layer; } } getStyleFor(layer, feature) { if (!this.styleByLayer[layer]) { return false; } for (const style of this.styleByLayer[layer]) { if (style.appliesTo(feature)) { return style; } } return false; } _replaceConstants(constants, tree) { for (const id in tree) { const node = tree[id]; switch (typeof node) { case 'object': if (node.constructor.name.match(/Stream/)) { continue; } this._replaceConstants(constants, node); break; case 'string': if (node.charAt(0) === '@') { tree[id] = constants[node]; } } } } //TODO Better translation of the long cases. _compileFilter(filter) { let filters; switch (filter != null ? filter[0] : void 0) { case 'all': filter = filter.slice(1); filters = (() => { return filter.map((sub) => this._compileFilter(sub)); }).call(this); return (feature) => !!filters.find((appliesTo) => { return !appliesTo(feature); }); case 'any': filter = filter.slice(1); filters = (() => { return filter.map((sub) => this._compileFilter(sub)); }).call(this); return (feature) => !!filters.find((appliesTo) => { return appliesTo(feature); }); case 'none': filter = filter.slice(1); filters = (() => { return filter.map((sub) => this._compileFilter(sub)); }).call(this); return (feature) => !filters.find((appliesTo) => { return !appliesTo(feature); }); case '==': return (feature) => feature.properties[filter[1]] === filter[2]; case '!=': return (feature) => feature.properties[filter[1]] !== filter[2]; case 'in': return (feature) => !!filter.slice(2).find((value) => { return feature.properties[filter[1]] === value; }); case '!in': return (feature) => !filter.slice(2).find((value) => { return feature.properties[filter[1]] === value; }); case 'has': return (feature) => !!feature.properties[filter[1]]; case '!has': return (feature) => !feature.properties[filter[1]]; case '>': return (feature) => feature.properties[filter[1]] > filter[2]; case '>=': return (feature) => feature.properties[filter[1]] >= filter[2]; case '<': return (feature) => feature.properties[filter[1]] < filter[2]; case '<=': return (feature) => feature.properties[filter[1]] <= filter[2]; default: return () => true; } } } module.exports = Styler; ================================================ FILE: src/Tile.js ================================================ /* termap - Terminal Map Viewer by Michael Strassburger 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'); const config = require('./config'); const utils = require('./utils'); 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; }); } _loadTile(buffer) { this.tile = new VectorTile(new Protobuf(buffer)); } _unzipIfNeeded(buffer) { return new Promise((resolve, reject) => { if (this._isGzipped(buffer)) { zlib.gunzip(buffer, (err, data) => { if (err) { reject(err); } resolve(data); }); } else { resolve(buffer); } }); } _isGzipped(buffer) { return buffer.slice(0, 2).indexOf(Buffer.from([0x1f, 0x8b])) === 0; } _loadLayers() { const layers = {}; const colorCache = {}; for (const name in this.tile.layers) { const layer = this.tile.layers[name]; const nodes = []; //continue if name is 'water' for (let i = 0; i < layer.length; i++) { // TODO: caching of similar attributes to avoid looking up the style each time //continue if @styler and not @styler.getStyleFor layer, feature const feature = layer.feature(i); feature.properties.$type = [undefined, 'Point', 'LineString', 'Polygon'][feature.type]; let style; if (this.styler) { style = this.styler.getStyleFor(name, feature); if (!style) { continue; } } let color = ( style.paint['line-color'] || style.paint['fill-color'] || style.paint['text-color'] ); // TODO: style zoom stops handling if (color instanceof Object) { color = color.stops[0][1]; } const colorCode = colorCache[color] || (colorCache[color] = x256(utils.hex2rgb(color))); // TODO: monkey patching test case for tiles with a reduced extent 4096 / 8 -> 512 // use feature.loadGeometry() again as soon as we got a 512 extent tileset const geometries = feature.loadGeometry(); //@_reduceGeometry feature, 8 const sort = feature.properties.localrank || feature.properties.scalerank; const label = style.type === 'symbol' ? feature.properties['name_' + config.language] || feature.properties.name_en || feature.properties.name || feature.properties.house_num : void 0; if (style.type === 'fill') { nodes.push(this._addBoundaries(true, { // id: feature.id layer: name, style, label, sort, points: geometries, color: colorCode, })); } else { for (const points of geometries) { nodes.push(this._addBoundaries(false, { //id: feature.id, layer: name, style, label, sort, points, color: colorCode, })); } } } const tree = new RBush(18); tree.load(nodes); layers[name] = { extent: layer.extent, tree, }; } return this.layers = layers; } _addBoundaries(deep, data) { let minX = 2e308; let maxX = -2e308; let minY = 2e308; let maxY = -2e308; const points = (deep ? data.points[0] : data.points); for (const p of points) { if (p.x < minX) minX = p.x; if (p.x > maxX) maxX = p.x; if (p.y < minY) minY = p.y; if (p.y > maxY) maxY = p.y; } data.minX = minX; data.maxX = maxX; data.minY = minY; data.maxY = maxY; return data; } _reduceGeometry(feature, factor) { const results = []; const geometries = feature.loadGeometry(); for (const points of geometries) { const reduced = []; let last; for (const point of points) { const p = { x: Math.floor(point.x / factor), y: Math.floor(point.y / factor) }; if (last && last.x === p.x && last.y === p.y) { continue; } reduced.push(last = p); } results.push(reduced); } return results; } } Tile.prototype.layers = {}; module.exports = Tile; ================================================ FILE: src/TileSource.js ================================================ /* termap - Terminal Map Viewer by Michael Strassburger Source for VectorTiles - supports * 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'); const paths = envPaths('mapscii'); const Tile = require('./Tile'); const config = require('./config'); // https://github.com/mapbox/node-mbtiles has native build dependencies (sqlite3) // To maximize MapSCII’s compatibility, MBTiles support must be manually added via // $> npm install -g @mapbox/mbtiles let MBTiles = null; try { MBTiles = require('@mapbox/mbtiles'); } catch (err) {void 0;} const modes = { MBTiles: 1, VectorTile: 2, HTTP: 3, }; class TileSource { init(source) { this.source = source; this.cache = {}; this.cacheSize = 16; this.cached = []; this.mode = null; this.mbtiles = null; this.styler = null; if (this.source.startsWith('http')) { if (config.persistDownloadedTiles) { this._initPersistence(); } this.mode = modes.HTTP; } else if (this.source.endsWith('.mbtiles')) { if (!MBTiles) { throw new Error('MBTiles support must be installed with following command: \'npm install -g @mapbox/mbtiles\''); } this.mode = modes.MBTiles; this.loadMBTiles(source); } else { throw new Error('source type isn\'t supported yet'); } } loadMBTiles(source) { return new Promise((resolve, reject) => { new MBTiles(source, (err, mbtiles) => { if (err) { reject(err); } this.mbtiles = mbtiles; resolve(); }); }); } useStyler(styler) { this.styler = styler; } getTile(z, x, y) { if (!this.mode) { throw new Error('no TileSource defined'); } const cached = this.cache[[z, x, y].join('-')]; if (cached) { return Promise.resolve(cached); } if (this.cached.length > this.cacheSize) { const overflow = Math.abs(this.cacheSize - this.cache.length); for (const tile in this.cached.splice(0, overflow)) { delete this.cache[tile]; } } switch (this.mode) { case modes.MBTiles: return this._getMBTile(z, x, y); case modes.HTTP: return this._getHTTP(z, x, y); } } _getHTTP(z, x, y) { let promise; const persistedTile = 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) => { if (config.persistDownloadedTiles) { this._persistTile(z, x, y, buffer); return buffer; } }); } return promise.then((buffer) => { return this._createTile(z, x, y, buffer); }); } _getMBTile(z, x, y) { return new Promise((resolve, reject) => { this.mbtiles.getTile(z, x, y, (err, buffer) => { if (err) { reject(err); } resolve(this._createTile(z, x, y, buffer)); }); }); } _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); } _initPersistence() { try { this._createFolder(paths.cache); } catch (error) { config.persistDownloadedTiles = false; } } _persistTile(z, x, y, buffer) { const zoom = z.toString(); this._createFolder(path.join(paths.cache, zoom)); const filePath = path.join(paths.cache, zoom, `${x}-${y}.pbf`); return fs.writeFile(filePath, buffer, () => null); } _getPersited(z, x, y) { try { return fs.readFileSync(path.join(paths.cache, z.toString(), `${x}-${y}.pbf`)); } catch (error) { return false; } } _createFolder(path) { try { fs.mkdirSync(path); return true; } catch (error) { if (error.code === 'EEXIST') return true; throw error; } } } module.exports = TileSource; ================================================ FILE: src/TileSource.spec.js ================================================ 'use strict'; const TileSource = require('./TileSource'); describe('TileSource', () => { describe('with a HTTP source', () => { test('sets the mode to 3', async () => { const tileSource = new TileSource(); await tileSource.init('http://mapscii.me/'); expect(tileSource.mode).toBe(3); }); }); }); ================================================ FILE: src/config.js ================================================ module.exports = { language: 'en', // TODO: adapt to osm2vectortiles successor openmaptiles v3) // mapscii.me hosts the last available version, 2016-06-20 source: 'http://mapscii.me/', //source: __dirname+"/../mbtiles/regensburg.mbtiles", styleFile: __dirname+'/../styles/dark.json', initialZoom: null, maxZoom: 18, zoomStep: 0.2, // sf lat: 37.787946, lon: -122.407522 // iceland lat: 64.124229, lon: -21.811552 // rgbg // lat: 49.019493, lon: 12.098341 initialLat: 52.51298, initialLon: 13.42012, simplifyPolylines: false, useBraille: true, // Downloaded files get persisted in ~/.mapscii persistDownloadedTiles: true, tileRange: 14, projectSize: 256, labelMargin: 5, layers: { housenum_label: { margin: 4 }, poi_label: { cluster: true, margin: 5, }, place_label: { cluster: true, }, state_label: { cluster: true, }, }, input: process.stdin, output: process.stdout, headless: false, delimeter: '\n\r', poiMarker: '◉', }; ================================================ FILE: src/utils.js ================================================ /* termap - Terminal Map Viewer by Michael Strassburger methods used all around */ 'use strict'; const config = require('./config'); 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; }, }; module.exports = utils; ================================================ FILE: src/utils.spec.js ================================================ 'use strict'; const utils = require('./utils'); describe('utils', () => { describe('hex2rgb', () => { describe.each([ ['#ff0000', 255, 0, 0], ['#ffff00', 255, 255, 0], ['#0000ff', 0, 0, 255], ['#112233', 17, 34, 51], ['#888', 136, 136, 136], ])('when given "%s"', (input, r, g, b) => { test(`returns [${r},${g},${b}]`, () => { expect(utils.hex2rgb(input)).toEqual([r, g, b]); }); }); test('throws an Error when given "33"', () => { function wrapper() { utils.hex2rgb('33'); } 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, }; expect(utils.normalize(input)).toEqual(expected); }); }); }); ================================================ FILE: styles/bright.json ================================================ { "name": "Bright", "constants": { "@background": "#000", "@water": "#5f87ff", "@building": "#99b", "@housenum_label": "#88a", "@country_label_1": "#ff0", "@country_label_2": "#ff0", "@country_label_3": "#ff0", "@country_label_4": "#ff0", "@admin_level_2": "#fff", "@admin_level_3": "#aac", "@admin_level_4": "#777", "@poi_label_1": "#ff0", "@poi_label_2": "#ee0", "@poi_label_3": "#cc0", "@poi_label_4": "#aa0", "@place_label_city": "#f00", "@place_label_town": "#d33", "@place_label_village": "#c33", "@place_label_other": "#b33" }, "layers": [ { "type": "background", "id": "background", "paint": { "background-color": "@background" } }, { "type": "fill", "id": "landuse_hospital", "paint": { "fill-color": "#fde" }, "source-layer": "landuse", "filter": [ "==", "class", "hospital" ] }, { "type": "line", "id": "waterway", "paint": { "line-color": "#303090", "line-width": { "base": 1.3, "stops": [ [ 13, 0.5 ], [ 20, 2 ] ] } }, "source-layer": "waterway", "filter": [ "all", [ "!=", "class", "river" ], [ "!=", "class", "stream" ], [ "!=", "class", "canal" ] ] }, { "type": "line", "id": "waterway_river", "paint": { "line-color": "#303090", "line-width": { "base": 1.2, "stops": [ [ 11, 0.5 ], [ 20, 6 ] ] } }, "source-layer": "waterway", "filter": [ "==", "class", "river" ] }, { "type": "line", "id": "waterway_stream_canal", "paint": { "line-color": "#a0c8f0", "line-width": { "base": 1.3, "stops": [ [ 13, 0.5 ], [ 20, 6 ] ] } }, "source-layer": "waterway", "filter": [ "in", "class", "stream", "canal" ] }, { "type": "fill", "id": "water", "paint": { "fill-color": "@water" }, "source-layer": "water" }, { "type": "fill", "id": "aeroway_fill", "paint": { "fill-color": "#f0ede9" }, "source-layer": "aeroway", "filter": [ "==", "$type", "Polygon" ], "minzoom": 11 }, { "type": "line", "id": "aeroway_runway", "paint": { "line-color": "#f0ede9", "line-width": { "base": 1.2, "stops": [ [ 11, 3 ], [ 20, 16 ] ] } }, "source-layer": "aeroway", "filter": [ "all", [ "==", "$type", "LineString" ], [ "==", "type", "runway" ] ], "minzoom": 11 }, { "type": "line", "id": "aeroway_taxiway", "paint": { "line-color": "#f0ede9", "line-width": { "base": 1.2, "stops": [ [ 11, 0.5 ], [ 20, 6 ] ] } }, "source-layer": "aeroway", "filter": [ "all", [ "==", "$type", "LineString" ], [ "==", "type", "taxiway" ] ], "minzoom": 11 }, { "type": "line", "id": "building", "paint": { "line-color": "@building" }, "source-layer": "building", "minzoom": 14.5 }, { "type": "line", "id": "tunnel_motorway_link_casing", "paint": { "line-color": "#e9ac77", "line-width": { "base": 1.2, "stops": [ [ 12, 1 ], [ 13, 3 ], [ 14, 4 ], [ 20, 15 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "tunnel" ], [ "==", "class", "motorway_link" ] ] }, { "type": "line", "id": "tunnel_service_track_casing", "paint": { "line-color": "#cfcdca", "line-width": { "base": 1.2, "stops": [ [ 15, 1 ], [ 16, 4 ], [ 20, 11 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "tunnel" ], [ "in", "class", "service", "track" ] ] }, { "type": "line", "id": "tunnel_link_casing", "paint": { "line-color": "#e9ac77", "line-width": { "base": 1.2, "stops": [ [ 12, 1 ], [ 13, 3 ], [ 14, 4 ], [ 20, 15 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "tunnel" ], [ "==", "class", "link" ] ] }, { "type": "line", "id": "tunnel_street_casing", "paint": { "line-color": "#cfcdca", "line-width": { "base": 1.2, "stops": [ [ 12, 0.5 ], [ 13, 1 ], [ 14, 4 ], [ 20, 15 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "tunnel" ], [ "in", "class", "street", "street_limited" ] ] }, { "type": "line", "id": "tunnel_secondary_tertiary_casing", "paint": { "line-color": "#e9ac77", "line-width": { "base": 1.2, "stops": [ [ 8, 1.5 ], [ 20, 17 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "tunnel" ], [ "in", "class", "secondary", "tertiary" ] ] }, { "type": "line", "id": "tunnel_trunk_primary_casing", "paint": { "line-color": "#e9ac77", "line-width": { "base": 1.2, "stops": [ [ 5, 0.4 ], [ 6, 0.6 ], [ 7, 1.5 ], [ 20, 22 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "tunnel" ], [ "in", "class", "trunk", "primary" ] ] }, { "type": "line", "id": "tunnel_motorway_casing", "paint": { "line-color": "#e9ac77", "line-width": { "base": 1.2, "stops": [ [ 5, 0.4 ], [ 6, 0.6 ], [ 7, 1.5 ], [ 20, 22 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "tunnel" ], [ "==", "class", "motorway" ] ] }, { "type": "line", "id": "tunnel_path_pedestrian", "paint": { "line-color": "#cba", "line-width": { "base": 1.2, "stops": [ [ 15, 1.2 ], [ 20, 4 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "$type", "LineString" ], [ "all", [ "==", "structure", "tunnel" ], [ "in", "class", "path", "pedestrian" ] ] ] }, { "type": "line", "id": "tunnel_major_rail", "paint": { "line-color": "#bbb", "line-width": { "base": 1.4, "stops": [ [ 14, 0.4 ], [ 15, 0.75 ], [ 20, 2 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "tunnel" ], [ "in", "class", "major_rail", "minor_rail" ] ] }, { "type": "line", "id": "road_motorway_link_casing", "paint": { "line-color": "#e9ac77", "line-width": { "base": 1.2, "stops": [ [ 12, 1 ], [ 13, 3 ], [ 14, 4 ], [ 20, 15 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "class", "motorway_link" ], [ "!in", "structure", "bridge", "tunnel" ] ], "minzoom": 12 }, { "type": "line", "id": "road_service_track_casing", "paint": { "line-color": "#cfcdca", "line-width": { "base": 1.2, "stops": [ [ 15, 1 ], [ 16, 4 ], [ 20, 11 ] ] } }, "source-layer": "road", "filter": [ "all", [ "in", "class", "service", "track" ], [ "!in", "structure", "bridge", "tunnel" ] ] }, { "type": "line", "id": "road_link_casing", "paint": { "line-color": "#e9ac77", "line-width": { "base": 1.2, "stops": [ [ 12, 1 ], [ 13, 3 ], [ 14, 4 ], [ 20, 15 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "class", "link" ], [ "!in", "structure", "bridge", "tunnel" ] ], "minzoom": 13 }, { "type": "line", "id": "road_street_casing", "paint": { "line-color": "#cfcdca", "line-width": { "base": 1.2, "stops": [ [ 12, 0.5 ], [ 13, 1 ], [ 14, 4 ], [ 20, 15 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "$type", "LineString" ], [ "all", [ "in", "class", "street", "street_limited" ], [ "!in", "structure", "bridge", "tunnel" ] ] ] }, { "type": "line", "id": "road_secondary_tertiary_casing", "paint": { "line-color": "#e9ac77", "line-width": { "base": 1.2, "stops": [ [ 8, 1.5 ], [ 20, 17 ] ] } }, "source-layer": "road", "filter": [ "all", [ "in", "class", "secondary", "tertiary" ], [ "!in", "structure", "bridge", "tunnel" ] ] }, { "type": "line", "id": "road_trunk_primary_casing", "paint": { "line-color": "#e9ac77", "line-width": { "base": 1.2, "stops": [ [ 5, 0.4 ], [ 6, 0.6 ], [ 7, 1.5 ], [ 20, 22 ] ] } }, "source-layer": "road", "filter": [ "all", [ "in", "class", "trunk", "primary" ], [ "!in", "structure", "bridge", "tunnel" ] ] }, { "type": "line", "id": "road_motorway_casing", "paint": { "line-color": "#e9ac77", "line-width": { "base": 1.2, "stops": [ [ 5, 0.4 ], [ 6, 0.6 ], [ 7, 1.5 ], [ 20, 22 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "class", "motorway" ], [ "!in", "structure", "bridge", "tunnel" ] ], "minzoom": 5 }, { "type": "line", "id": "road_path_pedestrian", "paint": { "line-color": "#cba", "line-width": { "base": 1.2, "stops": [ [ 15, 1.2 ], [ 20, 4 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "$type", "LineString" ], [ "all", [ "in", "class", "path", "pedestrian" ], [ "!in", "structure", "bridge", "tunnel" ] ] ] }, { "type": "line", "id": "road_major_rail", "paint": { "line-color": "#bbb", "line-width": { "base": 1.4, "stops": [ [ 14, 0.4 ], [ 15, 0.75 ], [ 20, 2 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "class", "major_rail" ], [ "!in", "structure", "bridge", "tunnel" ] ] }, { "type": "line", "id": "bridge_motorway_link_casing", "paint": { "line-color": "#e9ac77", "line-width": { "base": 1.2, "stops": [ [ 12, 1 ], [ 13, 3 ], [ 14, 4 ], [ 20, 15 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "bridge" ], [ "==", "class", "motorway_link" ] ] }, { "type": "line", "id": "bridge_service_track_casing", "paint": { "line-color": "#cfcdca", "line-width": { "base": 1.2, "stops": [ [ 15, 1 ], [ 16, 4 ], [ 20, 11 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "bridge" ], [ "in", "class", "service", "track" ] ] }, { "type": "line", "id": "bridge_link_casing", "paint": { "line-color": "#e9ac77", "line-width": { "base": 1.2, "stops": [ [ 12, 1 ], [ 13, 3 ], [ 14, 4 ], [ 20, 15 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "bridge" ], [ "==", "class", "link" ] ] }, { "type": "line", "id": "bridge_street_casing", "paint": { "line-color": "#cfcdca", "line-width": { "base": 1.2, "stops": [ [ 12, 0.5 ], [ 13, 1 ], [ 14, 4 ], [ 20, 15 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "bridge" ], [ "in", "class", "street", "street_limited" ] ] }, { "type": "line", "id": "bridge_secondary_tertiary_casing", "paint": { "line-color": "#e9ac77", "line-width": { "base": 1.2, "stops": [ [ 8, 1.5 ], [ 20, 17 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "bridge" ], [ "in", "class", "secondary", "tertiary" ] ] }, { "type": "line", "id": "bridge_trunk_primary_casing", "paint": { "line-color": "#e9ac77", "line-width": { "base": 1.2, "stops": [ [ 5, 0.4 ], [ 6, 0.6 ], [ 7, 1.5 ], [ 20, 22 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "bridge" ], [ "in", "class", "trunk", "primary" ] ] }, { "type": "line", "id": "bridge_motorway_casing", "paint": { "line-color": "#e9ac77", "line-width": { "base": 1.2, "stops": [ [ 5, 0.4 ], [ 6, 0.6 ], [ 7, 1.5 ], [ 20, 22 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "bridge" ], [ "==", "class", "motorway" ] ] }, { "type": "line", "id": "bridge_path_pedestrian", "paint": { "line-color": "#cba", "line-width": { "base": 1.2, "stops": [ [ 15, 1.2 ], [ 20, 4 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "$type", "LineString" ], [ "all", [ "==", "structure", "bridge" ], [ "in", "class", "path", "pedestrian" ] ] ] }, { "type": "line", "id": "bridge_major_rail", "paint": { "line-color": "#bbb", "line-width": { "base": 1.4, "stops": [ [ 14, 0.4 ], [ 15, 0.75 ], [ 20, 2 ] ] } }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "bridge" ], [ "==", "class", "major_rail" ] ] }, { "type": "line", "id": "admin_level_4", "paint": { "line-color": "@admin_level_4", "line-width": 1 }, "source-layer": "admin", "filter": [ "all", [ ">=", "admin_level", 4 ], [ "==", "maritime", 0 ] ] }, { "type": "line", "id": "admin_level_3", "paint": { "line-color": "@admin_level_3", "line-width": 1 }, "source-layer": "admin", "filter": [ "all", [ "==", "admin_level", 3 ], [ "==", "maritime", 0 ] ] }, { "type": "line", "id": "admin_level_2", "paint": { "line-color": "@admin_level_2", "line-width": { "base": 1, "stops": [ [ 4, 1.4 ], [ 5, 2 ], [ 12, 8 ] ] } }, "source-layer": "admin", "filter": [ "all", [ "==", "admin_level", 2 ], [ "==", "disputed", 0 ], [ "==", "maritime", 0 ] ] }, { "type": "line", "id": "admin_level_2_disputed", "paint": { "line-color": "#9e9cab", "line-width": { "base": 1, "stops": [ [ 4, 1.4 ], [ 5, 2 ], [ 12, 8 ] ] } }, "source-layer": "admin", "filter": [ "all", [ "==", "admin_level", 2 ], [ "==", "disputed", 1 ], [ "==", "maritime", 0 ] ] }, { "type": "symbol", "id": "water_label", "paint": { "text-color": "#74aee9" }, "source-layer": "water_label", "filter": [ "==", "$type", "Point" ], "layout": { "text-size": 12 } }, { "type": "symbol", "id": "poi_label_4", "paint": { "text-color": "@poi_label_4" }, "source-layer": "poi_label", "filter": [ "all", [ "==", "$type", "Point" ], [ "==", "scalerank", 4 ] ], "minzoom": 16, "layout": { "text-size": 12 } }, { "type": "symbol", "id": "poi_label_3", "paint": { "text-color": "@poi_label_3" }, "source-layer": "poi_label", "filter": [ "all", [ "==", "$type", "Point" ], [ "==", "scalerank", 3 ] ], "minzoom": 15, "layout": { "text-size": 12 } }, { "type": "symbol", "id": "poi_label_2", "paint": { "text-color": "@poi_label_2" }, "source-layer": "poi_label", "filter": [ "all", [ "==", "$type", "Point" ], [ "==", "scalerank", 2 ] ], "minzoom": 14, "layout": { "text-size": 12 } }, { "type": "symbol", "id": "rail_station_label", "paint": { "text-color": "#666" }, "source-layer": "rail_station_label", "layout": { "text-size": 12 } }, { "type": "symbol", "id": "poi_label_1", "paint": { "text-color": "@poi_label_1" }, "source-layer": "poi_label", "filter": [ "all", [ "==", "$type", "Point" ], [ "==", "scalerank", 1 ] ], "minzoom": 13, "layout": { "text-size": 12 } }, { "type": "symbol", "id": "airport_label", "paint": { "text-color": "#666" }, "source-layer": "airport_label", "filter": [ "all", [ "==", "$type", "Point" ], [ "in", "scalerank", 1, 2, 3 ] ], "minzoom": 11, "layout": { "text-size": 12 } }, { "type": "symbol", "id": "road_label", "paint": { "text-color": "#765" }, "source-layer": "road_label", "filter": [ "!=", "class", "ferry" ], "layout": { "text-size": { "base": 1, "stops": [ [ 13, 12 ], [ 14, 13 ] ] } } }, { "type": "symbol", "id": "housenum_label", "paint": { "text-color": "@housenum_label" }, "minzoom": 16.5, "source-layer": "housenum_label" }, { "type": "symbol", "id": "place_label_other", "paint": { "text-color": "@place_label_other" }, "source-layer": "place_label", "filter": [ "in", "type", "hamlet", "suburb", "neighbourhood", "island", "islet" ], "layout": { "text-size": { "base": 1.2, "stops": [ [ 12, 10 ], [ 15, 14 ] ] } } }, { "type": "symbol", "id": "place_label_village", "paint": { "text-color": "@place_label_village" }, "source-layer": "place_label", "filter": [ "==", "type", "village" ], "layout": { "text-size": { "base": 1.2, "stops": [ [ 10, 12 ], [ 15, 22 ] ] } } }, { "type": "symbol", "id": "place_label_town", "paint": { "text-color": "@place_label_town" }, "source-layer": "place_label", "filter": [ "==", "type", "town" ], "layout": { "text-size": { "base": 1.2, "stops": [ [ 10, 14 ], [ 15, 24 ] ] } } }, { "type": "symbol", "id": "place_label_city", "paint": { "text-color": "@place_label_city" }, "source-layer": "place_label", "filter": [ "==", "type", "city" ], "layout": { "text-size": { "base": 1.2, "stops": [ [ 7, 14 ], [ 11, 24 ] ] } } }, { "type": "symbol", "id": "marine_label_line_4", "paint": { "text-color": "#74aee9" }, "source-layer": "marine_label", "filter": [ "all", [ "==", "$type", "LineString" ], [ ">=", "labelrank", 4 ] ], "layout": { "text-size": { "stops": [ [ 3, 11 ], [ 4, 12 ] ] } } }, { "type": "symbol", "id": "marine_label_4", "paint": { "text-color": "#74aee9" }, "source-layer": "marine_label", "filter": [ "all", [ "==", "$type", "Point" ], [ ">=", "labelrank", 4 ] ], "layout": { "text-size": { "stops": [ [ 3, 11 ], [ 4, 12 ] ] } } }, { "type": "symbol", "id": "marine_label_line_3", "paint": { "text-color": "#74aee9" }, "source-layer": "marine_label", "filter": [ "all", [ "==", "$type", "LineString" ], [ "==", "labelrank", 3 ] ], "layout": { "text-size": { "stops": [ [ 3, 11 ], [ 4, 14 ] ] } } }, { "type": "symbol", "id": "marine_label_point_3", "paint": { "text-color": "#74aee9" }, "source-layer": "marine_label", "filter": [ "all", [ "==", "$type", "Point" ], [ "==", "labelrank", 3 ] ], "layout": { "text-size": { "stops": [ [ 3, 11 ], [ 4, 14 ] ] } } }, { "type": "symbol", "id": "marine_label_line_2", "paint": { "text-color": "#74aee9" }, "source-layer": "marine_label", "filter": [ "all", [ "==", "$type", "LineString" ], [ "==", "labelrank", 2 ] ], "layout": { "text-size": { "stops": [ [ 3, 14 ], [ 4, 16 ] ] } } }, { "type": "symbol", "id": "marine_label_point_2", "paint": { "text-color": "#74aee9" }, "source-layer": "marine_label", "filter": [ "all", [ "==", "$type", "Point" ], [ "==", "labelrank", 2 ] ], "layout": { "text-size": { "stops": [ [ 3, 14 ], [ 4, 16 ] ] } } }, { "type": "symbol", "id": "marine_label_line_1", "paint": { "text-color": "#74aee9" }, "source-layer": "marine_label", "filter": [ "all", [ "==", "$type", "LineString" ], [ "==", "labelrank", 1 ] ], "layout": { "text-size": { "stops": [ [ 3, 18 ], [ 4, 22 ] ] } } }, { "type": "symbol", "id": "marine_label_point_1", "paint": { "text-color": "#74aee9" }, "source-layer": "marine_label", "filter": [ "all", [ "==", "$type", "Point" ], [ "==", "labelrank", 1 ] ], "layout": { "text-size": { "stops": [ [ 3, 18 ], [ 4, 22 ] ] } } }, { "type": "symbol", "id": "country_label_4", "paint": { "text-color": "@country_label_4" }, "source-layer": "country_label", "filter": [ ">=", "scalerank", 4 ], "layout": { "text-size": { "stops": [ [ 4, 11 ], [ 6, 15 ] ] } } }, { "type": "symbol", "id": "country_label_3", "paint": { "text-color": "@country_label_3" }, "source-layer": "country_label", "filter": [ "==", "scalerank", 3 ], "layout": { "text-size": { "stops": [ [ 3, 11 ], [ 7, 17 ] ] } } }, { "type": "symbol", "id": "country_label_2", "paint": { "text-color": "@country_label_2" }, "source-layer": "country_label", "filter": [ "==", "scalerank", 2 ], "layout": { "text-size": { "stops": [ [ 2, 11 ], [ 5, 17 ] ] } } }, { "type": "symbol", "id": "country_label_1", "paint": { "text-color": "@country_label_1" }, "source-layer": "country_label", "filter": [ "==", "scalerank", 1 ], "layout": { "text-size": { "stops": [ [ 1, 11 ], [ 4, 17 ] ] } } } ] } ================================================ FILE: styles/dark.json ================================================ { "name": "dark", "constants": { "@admin_level_2": "#fff", "@admin_level_2_disputed": "#fff", "@admin_level_2_maritime": "#9bf", "@admin_level_3": "#aac", "@admin_level_3_maritime": "#8af", "@admin_level_4": "#777", "@aeroway_fill": "#f0ede9", "@aeroway_runway": "#f0ede9", "@aeroway_taxiway": "#f0ede9", "@airport_label": "#666", "@background": "#000", "@bridge_link": "#fea", "@bridge_major_rail": "#bbb", "@bridge_major_rail_hatching": "#bbb", "@bridge_motorway": "#fc8", "@bridge_motorway_link": "#fc8", "@bridge_path_pedestrian": "#cba", "@bridge_secondary_tertiary": "#fea", "@bridge_service_track": "#fff", "@bridge_street": "#fff", "@bridge_trunk_primary": "#fea", "@building": "#99b", "@country_label_1": "#ff0", "@country_label_2": "#ff0", "@country_label_3": "#ff0", "@country_label_4": "#ff0", "@landuse_cemetery": "#e0e4dd", "@landuse_hospital": "#d9b", "@landuse_overlay_national_park": "#d8e8c8", "@landuse_park": "#7b5", "@landuse_school": "#f0e8f8", "@landuse_wood": "#6a4", "@marine_label_4": "#74aee9", "@marine_label_line_1": "#74aee9", "@marine_label_line_2": "#74aee9", "@marine_label_line_3": "#74aee9", "@marine_label_line_4": "#74aee9", "@marine_label_point_1": "#74aee9", "@marine_label_point_2": "#74aee9", "@marine_label_point_3": "#74aee9", "@place_label_city": "#f00", "@place_label_town": "#d33", "@place_label_village": "#c33", "@place_label_other": "#b33", "@poi_label_1": "#ff0", "@poi_label_2": "#ee0", "@poi_label_3": "#cc0", "@poi_label_4": "#aa0", "@rail_station_label": "#666", "@road_label": "#765", "@road_link": "#fea", "@road_major_rail": "#bbb", "@road_major_rail_hatching": "#bbb", "@road_motorway": "#fc8", "@road_motorway_link": "#fc8", "@road_path_pedestrian": "#cba", "@road_secondary_tertiary": "#fea", "@road_service_track": "#fff", "@road_street": "#fff", "@road_trunk_primary": "#fea", "@tunnel_link": "#fff4c6", "@tunnel_major_rail": "#bbb", "@tunnel_major_rail_hatching": "#bbb", "@tunnel_motorway": "#ffdaa6", "@tunnel_motorway_link": "#fc8", "@tunnel_path_pedestrian": "#cba", "@tunnel_secondary_tertiary": "#fff4c6", "@tunnel_service_track": "#fff", "@tunnel_street": "#fff", "@tunnel_trunk_primary": "#fff4c6", "@water": "#5f87ff", "@water_label": "#74aee9", "@waterway": "#a0c8f0", "@waterway_river": "#a0c8f0", "@waterway_stream_canal": "#a0c8f0" }, "layers": [ { "type": "background", "id": "background", "paint": { "background-color": "@background" } }, { "type": "fill", "id": "landuse_overlay_national_park", "paint": { "fill-color": "@landuse_overlay_national_park" }, "source-layer": "landuse_overlay", "filter": [ "==", "class", "national_park" ] }, { "type": "fill", "id": "landuse_park", "paint": { "fill-color": "@landuse_park" }, "source-layer": "landuse", "filter": [ "==", "class", "park" ] }, { "type": "line", "id": "landuse_cemetery", "paint": { "fill-color": "@landuse_cemetery" }, "source-layer": "landuse", "filter": [ "==", "class", "cemetery" ] }, { "type": "line", "id": "landuse_hospital", "paint": { "line-color": "@landuse_hospital" }, "source-layer": "landuse", "filter": [ "==", "class", "hospital" ] }, { "type": "line", "id": "landuse_school", "paint": { "line-color": "@landuse_school" }, "source-layer": "landuse", "filter": [ "==", "class", "school" ] }, { "type": "line", "id": "landuse_wood", "paint": { "line-color": "@landuse_wood" }, "source-layer": "landuse", "filter": [ "==", "class", "wood" ] }, { "type": "line", "id": "waterway", "paint": { "line-color": "@waterway" }, "source-layer": "waterway", "filter": [ "all", [ "!=", "class", "river" ], [ "!=", "class", "stream" ], [ "!=", "class", "canal" ] ] }, { "type": "line", "id": "waterway_river", "paint": { "line-color": "@waterway_river" }, "source-layer": "waterway", "filter": [ "==", "class", "river" ] }, { "type": "line", "id": "waterway_stream_canal", "paint": { "line-color": "@waterway_stream_canal" }, "source-layer": "waterway", "filter": [ "in", "class", "stream", "canal" ] }, { "type": "fill", "id": "water", "paint": { "fill-color": "@water" }, "source-layer": "water" }, { "type": "fill", "id": "aeroway_fill", "paint": { "fill-color": "@aeroway_fill" }, "source-layer": "aeroway", "filter": [ "==", "$type", "Polygon" ], "minzoom": 11 }, { "type": "line", "id": "aeroway_runway", "paint": { "line-color": "@aeroway_runway" }, "source-layer": "aeroway", "filter": [ "all", [ "==", "$type", "LineString" ], [ "==", "type", "runway" ] ], "minzoom": 11 }, { "type": "line", "id": "aeroway_taxiway", "paint": { "line-color": "@aeroway_taxiway" }, "source-layer": "aeroway", "filter": [ "all", [ "==", "$type", "LineString" ], [ "==", "type", "taxiway" ] ], "minzoom": 11 }, { "type": "line", "id": "building", "paint": { "line-color": "@building" }, "source-layer": "building" }, { "type": "line", "id": "tunnel_path_pedestrian", "paint": { "line-color": "@tunnel_path_pedestrian" }, "source-layer": "road", "filter": [ "all", [ "==", "$type", "LineString" ], [ "all", [ "==", "structure", "tunnel" ], [ "in", "class", "path", "pedestrian" ] ] ] }, { "type": "line", "id": "tunnel_motorway_link", "paint": { "line-color": "@tunnel_motorway_link" }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "tunnel" ], [ "==", "class", "motorway_link" ] ] }, { "type": "line", "id": "tunnel_service_track", "paint": { "line-color": "@tunnel_service_track" }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "tunnel" ], [ "in", "class", "service", "track" ] ] }, { "type": "line", "id": "tunnel_link", "paint": { "line-color": "@tunnel_link" }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "tunnel" ], [ "==", "class", "link" ] ] }, { "type": "line", "id": "tunnel_street", "paint": { "line-color": "@tunnel_street" }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "tunnel" ], [ "in", "class", "street", "street_limited" ] ] }, { "type": "line", "id": "tunnel_secondary_tertiary", "paint": { "line-color": "@tunnel_secondary_tertiary" }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "tunnel" ], [ "in", "class", "secondary", "tertiary" ] ] }, { "type": "line", "id": "tunnel_trunk_primary", "paint": { "line-color": "@tunnel_trunk_primary" }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "tunnel" ], [ "in", "class", "trunk", "primary" ] ] }, { "type": "line", "id": "tunnel_motorway", "paint": { "line-color": "@tunnel_motorway" }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "tunnel" ], [ "==", "class", "motorway" ] ] }, { "type": "line", "id": "tunnel_major_rail", "paint": { "line-color": "@tunnel_major_rail" }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "tunnel" ], [ "in", "class", "major_rail", "minor_rail" ] ] }, { "type": "line", "id": "tunnel_major_rail_hatching", "paint": { "line-color": "@tunnel_major_rail_hatching" }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "tunnel" ], [ "in", "class", "major_rail", "minor_rail" ] ] }, { "type": "line", "id": "road_path_pedestrian", "paint": { "line-color": "@road_path_pedestrian" }, "source-layer": "road", "filter": [ "all", [ "==", "$type", "LineString" ], [ "all", [ "in", "class", "path", "pedestrian" ], [ "!in", "structure", "bridge", "tunnel" ] ] ] }, { "type": "line", "id": "road_motorway_link", "paint": { "line-color": "@road_motorway_link" }, "source-layer": "road", "minzoom": 12, "filter": [ "all", [ "==", "class", "motorway_link" ], [ "!in", "structure", "bridge", "tunnel" ] ] }, { "type": "line", "id": "road_service_track", "paint": { "line-color": "@road_service_track" }, "source-layer": "road", "filter": [ "all", [ "in", "class", "service", "track" ], [ "!in", "structure", "bridge", "tunnel" ] ] }, { "type": "line", "id": "road_link", "paint": { "line-color": "@road_link" }, "source-layer": "road", "minzoom": 13, "filter": [ "all", [ "==", "class", "link" ], [ "!in", "structure", "bridge", "tunnel" ] ] }, { "type": "line", "id": "road_street", "paint": { "line-color": "@road_street" }, "source-layer": "road", "filter": [ "all", [ "==", "$type", "LineString" ], [ "all", [ "in", "class", "street", "street_limited" ], [ "!in", "structure", "bridge", "tunnel" ] ] ] }, { "type": "line", "id": "road_secondary_tertiary", "paint": { "line-color": "@road_secondary_tertiary" }, "source-layer": "road", "filter": [ "all", [ "in", "class", "secondary", "tertiary" ], [ "!in", "structure", "bridge", "tunnel" ] ] }, { "type": "line", "id": "road_trunk_primary", "paint": { "line-color": "@road_trunk_primary" }, "source-layer": "road", "filter": [ "all", [ "in", "class", "trunk", "primary" ], [ "!in", "structure", "bridge", "tunnel" ] ] }, { "type": "line", "id": "road_motorway", "paint": { "line-color": "@road_motorway" }, "source-layer": "road", "minzoom": 5, "filter": [ "all", [ "==", "class", "motorway" ], [ "!in", "structure", "bridge", "tunnel" ] ] }, { "type": "line", "id": "road_major_rail", "paint": { "line-color": "@road_major_rail" }, "source-layer": "road", "filter": [ "all", [ "==", "class", "major_rail" ], [ "!in", "structure", "bridge", "tunnel" ] ] }, { "type": "line", "id": "road_major_rail_hatching", "paint": { "line-color": "@road_major_rail_hatching" }, "source-layer": "road", "filter": [ "all", [ "==", "class", "major_rail" ], [ "!in", "structure", "bridge", "tunnel" ] ] }, { "type": "line", "id": "bridge_path_pedestrian", "paint": { "line-color": "@bridge_path_pedestrian" }, "source-layer": "road", "filter": [ "all", [ "==", "$type", "LineString" ], [ "all", [ "==", "structure", "bridge" ], [ "in", "class", "path", "pedestrian" ] ] ] }, { "type": "line", "id": "bridge_motorway_link", "paint": { "line-color": "@bridge_motorway_link" }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "bridge" ], [ "==", "class", "motorway_link" ] ] }, { "type": "line", "id": "bridge_service_track", "paint": { "line-color": "@bridge_service_track" }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "bridge" ], [ "in", "class", "service", "track" ] ] }, { "type": "line", "id": "bridge_link", "paint": { "line-color": "@bridge_link" }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "bridge" ], [ "==", "class", "link" ] ] }, { "type": "line", "id": "bridge_street", "paint": { "line-color": "@bridge_street" }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "bridge" ], [ "in", "class", "street", "street_limited" ] ] }, { "type": "line", "id": "bridge_secondary_tertiary", "paint": { "line-color": "@bridge_secondary_tertiary" }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "bridge" ], [ "in", "class", "secondary", "tertiary" ] ] }, { "type": "line", "id": "bridge_trunk_primary", "paint": { "line-color": "@bridge_trunk_primary" }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "bridge" ], [ "in", "class", "trunk", "primary" ] ] }, { "type": "line", "id": "bridge_motorway", "paint": { "line-color": "@bridge_motorway" }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "bridge" ], [ "==", "class", "motorway" ] ] }, { "type": "line", "id": "bridge_major_rail", "paint": { "line-color": "@bridge_major_rail" }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "bridge" ], [ "==", "class", "major_rail" ] ] }, { "type": "line", "id": "bridge_major_rail_hatching", "paint": { "line-color": "@bridge_major_rail_hatching" }, "source-layer": "road", "filter": [ "all", [ "==", "structure", "bridge" ], [ "==", "class", "major_rail" ] ] }, { "type": "line", "id": "admin_level_3", "paint": { "line-color": "@admin_level_3" }, "source-layer": "admin", "filter": [ "all", [ ">=", "admin_level", 4 ], [ "==", "maritime", 0 ] ] }, { "type": "line", "id": "admin_level_3", "paint": { "line-color": "@admin_level_3" }, "source-layer": "admin", "filter": [ "all", [ "==", "admin_level", 3 ], [ "==", "maritime", 0 ] ] }, { "type": "line", "id": "admin_level_2", "paint": { "line-color": "@admin_level_2" }, "source-layer": "admin", "filter": [ "all", [ "==", "admin_level", 2 ], [ "==", "disputed", 0 ], [ "==", "maritime", 0 ] ] }, { "type": "line", "id": "admin_level_2_disputed", "paint": { "line-color": "@admin_level_2_disputed" }, "source-layer": "admin", "filter": [ "all", [ "==", "admin_level", 2 ], [ "==", "disputed", 1 ], [ "==", "maritime", 0 ] ] }, { "type": "line", "id": "admin_level_3_maritime", "paint": { "line-color": "@admin_level_3_maritime" }, "source-layer": "admin", "filter": [ "all", [ ">=", "admin_level", 3 ], [ "==", "maritime", 1 ] ] }, { "type": "line", "id": "admin_level_2_maritime", "paint": { "line-color": "@admin_level_2_maritime" }, "source-layer": "admin", "filter": [ "all", [ "==", "admin_level", 2 ], [ "==", "maritime", 1 ] ] }, { "type": "symbol", "id": "water_label", "paint": { "text-color": "@water_label" }, "source-layer": "water_label", "filter": [ "==", "$type", "Point" ] }, { "type": "symbol", "id": "poi_label_4", "paint": { "text-color": "@poi_label_4" }, "source-layer": "poi_label", "filter": [ "all", [ "==", "$type", "Point" ], [ "==", "scalerank", 4 ] ], "minzoom": 16 }, { "type": "symbol", "id": "poi_label_3", "paint": { "text-color": "@poi_label_3" }, "source-layer": "poi_label", "filter": [ "all", [ "==", "$type", "Point" ], [ "==", "scalerank", 3 ] ], "minzoom": 15 }, { "type": "symbol", "id": "poi_label_2", "paint": { "text-color": "@poi_label_2" }, "source-layer": "poi_label", "filter": [ "all", [ "==", "$type", "Point" ], [ "==", "scalerank", 2 ] ], "minzoom": 14 }, { "type": "symbol", "id": "rail_station_label", "paint": { "text-color": "@rail_station_label" }, "source-layer": "rail_station_label" }, { "type": "symbol", "id": "poi_label_1", "paint": { "text-color": "@poi_label_1" }, "source-layer": "poi_label", "filter": [ "all", [ "==", "$type", "Point" ], [ "==", "scalerank", 1 ] ], "minzoom": 13 }, { "type": "symbol", "id": "airport_label", "paint": { "text-color": "@airport_label" }, "source-layer": "airport_label", "filter": [ "all", [ "==", "$type", "Point" ], [ "in", "scalerank", 1, 2, 3 ] ], "minzoom": 11 }, { "type": "symbol", "id": "road_label", "paint": { "text-color": "@road_label" }, "source-layer": "road_label", "filter": [ "!=", "class", "ferry" ], "minzoom": 15.5 }, { "type": "symbol", "id": "place_label_other", "paint": { "text-color": "@place_label_other" }, "source-layer": "place_label", "filter": [ "in", "type", "hamlet", "suburb", "neighbourhood", "island", "islet" ] }, { "type": "symbol", "id": "place_label_village", "paint": { "text-color": "@place_label_village" }, "source-layer": "place_label", "filter": [ "==", "type", "village" ] }, { "type": "symbol", "id": "place_label_town", "paint": { "text-color": "@place_label_town" }, "source-layer": "place_label", "filter": [ "==", "type", "town" ] }, { "type": "symbol", "id": "place_label_city", "paint": { "text-color": "@place_label_city" }, "source-layer": "place_label", "filter": [ "==", "type", "city" ] }, { "type": "symbol", "id": "marine_label_line_4", "paint": { "text-color": "@marine_label_line_4" }, "source-layer": "marine_label", "filter": [ "all", [ "==", "$type", "LineString" ], [ ">=", "labelrank", 4 ] ] }, { "type": "symbol", "id": "marine_label_4", "paint": { "text-color": "@marine_label_4" }, "source-layer": "marine_label", "filter": [ "all", [ "==", "$type", "Point" ], [ ">=", "labelrank", 4 ] ] }, { "type": "symbol", "id": "marine_label_line_3", "paint": { "text-color": "@marine_label_line_3" }, "source-layer": "marine_label", "filter": [ "all", [ "==", "$type", "LineString" ], [ "==", "labelrank", 3 ] ] }, { "type": "symbol", "id": "marine_label_point_3", "paint": { "text-color": "@marine_label_point_3" }, "source-layer": "marine_label", "filter": [ "all", [ "==", "$type", "Point" ], [ "==", "labelrank", 3 ] ] }, { "type": "symbol", "id": "marine_label_line_2", "paint": { "text-color": "@marine_label_line_2" }, "source-layer": "marine_label", "filter": [ "all", [ "==", "$type", "LineString" ], [ "==", "labelrank", 2 ] ] }, { "type": "symbol", "id": "marine_label_point_2", "paint": { "text-color": "@marine_label_point_2" }, "source-layer": "marine_label", "filter": [ "all", [ "==", "$type", "Point" ], [ "==", "labelrank", 2 ] ] }, { "type": "symbol", "id": "marine_label_line_1", "paint": { "text-color": "@marine_label_line_1" }, "source-layer": "marine_label", "filter": [ "all", [ "==", "$type", "LineString" ], [ "==", "labelrank", 1 ] ] }, { "type": "symbol", "id": "marine_label_point_1", "paint": { "text-color": "@marine_label_point_1" }, "source-layer": "marine_label", "filter": [ "all", [ "==", "$type", "Point" ], [ "==", "labelrank", 1 ] ] }, { "type": "symbol", "id": "country_label_4", "paint": { "text-color": "@country_label_4" }, "source-layer": "country_label", "filter": [ ">=", "scalerank", 4 ] }, { "type": "symbol", "id": "country_label_3", "paint": { "text-color": "@country_label_3" }, "source-layer": "country_label", "filter": [ "==", "scalerank", 3 ] }, { "type": "symbol", "id": "country_label_2", "paint": { "text-color": "@country_label_2" }, "source-layer": "country_label", "filter": [ "==", "scalerank", 2 ] }, { "type": "symbol", "id": "country_label_1", "paint": { "text-color": "@country_label_1" }, "source-layer": "country_label", "filter": [ "==", "scalerank", 1 ] } ] }