kopia lustrzana https://github.com/rastapasta/mapscii
Porównaj commity
161 Commity
Autor | SHA1 | Data |
---|---|---|
dependabot[bot] | 4fe9a60a0c | |
dependabot[bot] | 1ac31f62e9 | |
dependabot[bot] | 84f29b8e8b | |
dependabot[bot] | dcc2508761 | |
dependabot[bot] | bae6fcfc93 | |
dependabot[bot] | e39cd8783b | |
dependabot[bot] | c093199c65 | |
dependabot[bot] | edeb0ebb2d | |
Christian Paul | 76f541db46 | |
dependabot[bot] | 2315a3515c | |
dependabot[bot] | 7f0c04cb49 | |
dependabot[bot] | c58da4e86f | |
dependabot[bot] | f2a694f26d | |
Michael Straßburger | 9f0fa46f92 | |
Michael Straßburger | da9d4d05a6 | |
dependabot[bot] | 64c53f6d01 | |
dependabot[bot] | dafe5cf4cb | |
dependabot[bot] | 9be22c8a1a | |
Christian Paul | b70e64f378 | |
Christian Paul | 247122f684 | |
Christian Paul | a7e7621705 | |
Christian Paul | 2d88b6f16d | |
Christian Paul | 0ce9b27a04 | |
Christian Paul | df269c10d9 | |
Christian Paul | 26f8ebe1f5 | |
Christian Paul | 5c8ded81dc | |
Christian Paul | 9eb2e5ef25 | |
Christian Paul | 9bee8cca19 | |
Thorkil Værge | 34d628d1d1 | |
dependabot[bot] | 219161e13e | |
Christian Paul | 393f867c84 | |
Christian Paul | 8475501605 | |
dependabot[bot] | 739176e817 | |
Michael Straßburger | 7103ecc0b7 | |
Michael Straßburger | 223bb9b644 | |
Michael Straßburger | c1cfe3ecfe | |
Michael Straßburger | 3b85c6d513 | |
Quincy Morgan | a872c0ee6d | |
Michael Straßburger | dcda97e305 | |
Michael Straßburger | 4011c877bb | |
Michael Straßburger | b76e626d5e | |
Christian Paul | b2e491d9ea | |
Christian Paul | b07786c547 | |
Christian Paul | 348b84f71f | |
Christian Paul | 184ce100d7 | |
0xflotus | 54235a81b7 | |
Christian Paul | 95ad703199 | |
Jestin Stoffel | fccacad3a2 | |
Christian Paul | 9b5769a2ec | |
Christian Paul | 421d71255f | |
Christian Paul | 7d4cd331ae | |
Christian Paul | eac7cb4d15 | |
Christian Paul | 22532ba03b | |
Christian Paul | 0b0384c3f3 | |
Christian Paul | e7ff94fc1c | |
Christian Paul | ff75d2de50 | |
Christian Paul | 6c86050b3d | |
Christian Paul | ad4103c8df | |
Christian Paul | 7b322af22f | |
Christian Paul | d98aced647 | |
Christian Paul | 044c24f6c8 | |
Christian Paul | 66a605254c | |
Christian Paul | 3dcebfa42e | |
Christian Paul | fea38767d3 | |
Christian Paul | eb1f81536e | |
Christian Paul | f4c002cadd | |
Christian Paul | 8a4d82dead | |
Christian Paul | 4546a99a55 | |
Christian Paul | cf4731682e | |
Christian Paul | 0ba7a7201d | |
Christian Paul | 4b97538235 | |
Christian Paul | ca789ba9dd | |
Christian Paul | 926a6d0bcc | |
Christian Paul | cdb0f45c75 | |
Christian Paul | b320618a7e | |
Christian Paul | e47fe83f38 | |
Christian Paul | 14628a80c2 | |
Christian Paul | 2a914dc983 | |
Christian Paul | 62440a98b8 | |
Christian Paul | 9ef0d88d55 | |
Christian Paul | baceee6ad6 | |
Christian Paul | 1480258a73 | |
Christian Paul | fb624c5784 | |
Christian Paul | 5e095f473a | |
Christian Paul | 7430ca09fa | |
Christian Paul | 5a384f6e25 | |
Christian Paul | 0642d2e533 | |
Christian Paul | 712898aa97 | |
Christian Paul | c03fb7fd64 | |
Christian Paul | 23d5b4db16 | |
Christian Paul | 24737f3db9 | |
Christian Paul | ac273b2409 | |
Christian Paul | 46dd1a9853 | |
Christian Paul | 25ce95a8b3 | |
Christian Paul | 9c66bb5418 | |
Christian Paul | 76d1995e95 | |
Christian Paul | 60abd91bba | |
Christian Paul | 2bf82b8f46 | |
Christian Paul | 7a313c8a7c | |
Christian Paul | a76679b8ac | |
Christian Paul | b704e21c4b | |
Christian Paul | 02dc03de96 | |
Christian Paul | c6d76ec41c | |
Alexander Meshcheryakov | 8440715e44 | |
Leo Arias | 43e589cbd1 | |
Christian Paul | be9b703720 | |
Christian Paul | 57497ecea7 | |
Christian Paul | 7d6aa54d33 | |
Christian Paul | a784a55759 | |
Christian Paul | f3efbae8ba | |
Christian Paul | bfd5c54fa4 | |
Christian Paul | 0353effa67 | |
Christian Paul | 0c27a79ad6 | |
Christian Paul | 069de8c227 | |
Christian Paul | 1f7245644e | |
Christian Paul | f6485d9830 | |
Christian Paul | d692c9317c | |
Christian Paul | ba09e8bd82 | |
Christian Paul | eee880e237 | |
Christian Paul | 8673da8ecb | |
Christian Paul | 804b55076d | |
Christian Paul | 088b9a3b71 | |
Christian Paul | 1369140af4 | |
Christian Paul | 09142e7a8f | |
Christian Paul | 4122e8826b | |
Christian Paul | bf8ebfd3df | |
Christian Paul | f236f95383 | |
Christian Paul | 283c245a88 | |
Christian Paul | 29b5b3ccb1 | |
Christian Paul | d27e086758 | |
Christian Paul | 9d9ef31f7b | |
Christian Paul | fd40b57f51 | |
Christian Paul | 554ae593be | |
Christian Paul | 2d22e33ff1 | |
Christian Paul | 6033f0be3c | |
Christian Paul | 48367926c2 | |
Christian Paul | ac784977d9 | |
Michael Straßburger | 1251e3f257 | |
Michael Straßburger | 45071fd931 | |
Michael Straßburger | b65da50d05 | |
Leo Arias | 490a598d84 | |
Michael Straßburger | 45315d4ccf | |
Alexander Zhukov | 5fcf553427 | |
Michael Straßburger | cb42c21812 | |
Michael Straßburger | 60dfdb8135 | |
Michael Straßburger | 5873af5c96 | |
Michael Straßburger | 724e605317 | |
Michael Straßburger | 7edde3b30f | |
Michael Straßburger | 7e255edc53 | |
Michael Straßburger | 82c05d6932 | |
Michael Straßburger | 46692ec1bb | |
Michael Straßburger | 03f191edb0 | |
Michael Straßburger | 7fc618d6d2 | |
Michael Straßburger | 3888293f0e | |
Michael Straßburger | ca6a4e27ae | |
Michael Straßburger | 3dffcde3b5 | |
Michael Straßburger | 0e5ce02e79 | |
Michael Straßburger | 0bdb01f706 | |
Michael Straßburger | e79d91186b | |
Michael Straßburger | 0b6d24126b | |
Michael Straßburger | cd5176d23b |
|
@ -0,0 +1,36 @@
|
|||
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"
|
||||
]
|
||||
}
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
name: "\U0001F41B Bug report"
|
||||
about: Something isn’t working as expected
|
||||
---
|
||||
|
||||
**Steps to reproduce**
|
||||
|
||||
<!--
|
||||
A step by step description of how to get to the error state.
|
||||
Are you using the the telnet or the local client?
|
||||
If you run MapSCII locally, what is your NodeJS version? (run `node -v`)
|
||||
It might help to know which operating system and keyboard language your are using.
|
||||
-->
|
||||
|
||||
**Current behavior**
|
||||
|
||||
<!--
|
||||
A clear and concise description of what the bug is.
|
||||
Please include any JavaScript errors
|
||||
-->
|
||||
|
||||
**Expected behavior**
|
||||
|
||||
<!--
|
||||
What did you expect to happen?
|
||||
-->
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
name: "\U0001F680 Feature request"
|
||||
about: I have a suggestion (and might want to implement myself)
|
||||
---
|
||||
|
||||
<!-- Consider opening a pull request instead: it’s a more productive way to discuss new features -->
|
||||
|
||||
**The problem**
|
||||
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
**Proposed solution**
|
||||
|
||||
<!-- A clear and concise description of what you want to happen. Add any considered drawbacks. -->
|
||||
|
||||
**Alternative solutions**
|
||||
|
||||
<!-- A clear and concise description of any alternative solutions or features you’ve considered. -->
|
||||
|
||||
**Additional context**
|
||||
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
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).
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
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).
|
|
@ -0,0 +1,9 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- "14"
|
||||
- "16"
|
||||
- "18"
|
||||
|
||||
script:
|
||||
- npm run-script lint
|
||||
- npm test
|
|
@ -0,0 +1,8 @@
|
|||
# This is the list of MapSCII authors for copyright purposes.
|
||||
#
|
||||
Michael Straßburger
|
||||
Christian Paul (https://chrpaul.de)
|
||||
Jannis R <mail@jannisr.de>
|
||||
Alexander Zhukov (https://github.com/ZhukovAlexander)
|
||||
Quincy Morgan (https://github.com/quincylvania)
|
||||
lennonhill (https://github.com/lennonhill)
|
3
LICENSE
3
LICENSE
|
@ -1,6 +1,7 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2016 Michael Straßburger
|
||||
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
|
||||
|
|
81
README.md
81
README.md
|
@ -1,24 +1,42 @@
|
|||
# MapSCII - The Whole World In Your Console.
|
||||
# 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.
|
||||
|
||||
<a href="https://asciinema.org/a/117813?autoplay=1" target="_blank">![asciicast](https://cloud.githubusercontent.com/assets/1259904/25480718/497a64e2-2b4a-11e7-9cf0-ed52ee0b89c0.png)</a>
|
||||
|
||||
## 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 [OpenStreetMap](https://en.wikipedia.org/wiki/OpenStreetMap) based one
|
||||
* 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 optimizied algorithms for a smooth experience
|
||||
* 100% pure Coffee-/JavaScript! :sunglasses:
|
||||
* Highly optimized algorithms for a smooth experience
|
||||
* 100% pure JavaScript! :sunglasses:
|
||||
|
||||
## How to install
|
||||
## How to run it locally
|
||||
|
||||
If you haven't already got Node.js >= version 4.5, then [go get it](http://nodejs.org/).
|
||||
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
|
||||
|
@ -26,6 +44,14 @@ 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.
|
||||
|
@ -38,6 +64,7 @@ mapscii
|
|||
|
||||
* 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
|
||||
|
@ -50,6 +77,7 @@ If your terminal supports mouse events you can drag the map and use your scroll
|
|||
* [`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
|
||||
|
@ -59,31 +87,27 @@ If your terminal supports mouse events you can drag the map and use your scroll
|
|||
#### 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
|
||||
* [`breseham`](https://github.com/madbence/node-bresenham) for line point calculations
|
||||
* [`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
|
||||
* [`bluebird`](https://github.com/petkaantonov/bluebird) for all the asynchronous [Promise](https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Promise) magic
|
||||
* [`node-fetch`](https://github.com/bitinn/node-fetch) for HTTP requests
|
||||
* [`userhome`](https://github.com/shama/userhome) to determine where to persist downloaded tiles
|
||||
* [`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
|
||||
* [ ] TileSource
|
||||
* [ ] Style
|
||||
* [ ] center position
|
||||
* [ ] zoom
|
||||
* [-] startup parameters
|
||||
* [X] TileSource
|
||||
* [X] Style
|
||||
* [X] center position
|
||||
* [X] zoom
|
||||
* [ ] demo mode?
|
||||
|
||||
* [ ] mouse control
|
||||
* [ ] get hover lat/lng
|
||||
* [ ] accurate mouse drag&drop with instant update
|
||||
* [ ] hover POIs/labels
|
||||
* [ ] hover maybe even polygons/-lines?
|
||||
* [ ] zoom into mouse pos
|
||||
|
||||
* Styler
|
||||
* [ ] respect zoom based style ranges
|
||||
|
@ -97,12 +121,23 @@ If your terminal supports mouse events you can drag the map and use your scroll
|
|||
* TileSource
|
||||
* [ ] implement single vector-tile handling
|
||||
|
||||
## License
|
||||
#### The MIT License (MIT)
|
||||
Copyright (c) 2017 Michael Straßburger
|
||||
## Special thanks
|
||||
|
||||
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:
|
||||
* [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), ..)
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
## Licenses
|
||||
|
||||
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.
|
||||
### 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)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/sh
|
||||
':' //; # Based on https://github.com/MrRio/vtop/blob/master/bin/vtop.js
|
||||
':' //; export TERM=xterm-256color
|
||||
':' //; exec "$(command -v nodejs || command -v node)" "$0" "$@"
|
||||
':' //; exec "$(command -v node || command -v nodejs)" "$0" "$@"
|
||||
'use strict'
|
||||
require('../main.js');
|
||||
|
|
82
main.js
82
main.js
|
@ -1,5 +1,5 @@
|
|||
/*#
|
||||
mapscii - Terminal Map Viewer
|
||||
MapSCII - Terminal Map Viewer
|
||||
by Michael Strassburger <codepoet@cpan.org>
|
||||
Discover the planet in your console!
|
||||
|
||||
|
@ -7,9 +7,81 @@
|
|||
|
||||
TODO: params parsing and so on
|
||||
#*/
|
||||
require('coffee-script/register');
|
||||
|
||||
'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;
|
||||
|
||||
mapscii = new Mapscii();
|
||||
mapscii.init();
|
||||
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);
|
||||
});
|
||||
|
|
Plik diff jest za duży
Load Diff
36
package.json
36
package.json
|
@ -1,11 +1,12 @@
|
|||
{
|
||||
"name": "mapscii",
|
||||
"version": "0.1.4",
|
||||
"description": "Map+Ascii -> MapSCII! Console Map Viewer.",
|
||||
"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": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "jest"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -15,7 +16,7 @@
|
|||
"mapscii": "./bin/mapscii.sh"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.5.0"
|
||||
"node": ">=10"
|
||||
},
|
||||
"keywords": [
|
||||
"map",
|
||||
|
@ -29,18 +30,23 @@
|
|||
"author": "Michael Straßburger <codepoet@cpan.org>",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bluebird": "^3.4.6",
|
||||
"@mapbox/vector-tile": "^1.3.1",
|
||||
"bresenham": "0.0.4",
|
||||
"coffee-script": "^1.10.0",
|
||||
"earcut": "^2.1.1",
|
||||
"earcut": "^2.2.2",
|
||||
"env-paths": "^2.2.0",
|
||||
"keypress": "^0.2.1",
|
||||
"node-fetch": "^1.6.3",
|
||||
"pbf": "^3.0.0",
|
||||
"rbush": "^2.0.1",
|
||||
"simplify-js": "^1.2.1",
|
||||
"term-mouse": "^0.1.1",
|
||||
"userhome": "^1.0.0",
|
||||
"vector-tile": "^1.3.0",
|
||||
"x256": "0.0.2"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
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
|
|
@ -1,105 +0,0 @@
|
|||
###
|
||||
termap - Terminal Map Viewer
|
||||
by Michael Strassburger <codepoet@cpan.org>
|
||||
|
||||
Simple pixel to barille character mapper
|
||||
|
||||
Implementation inspired by node-drawille (https://github.com/madbence/node-drawille)
|
||||
* added color support
|
||||
* added support for filled polygons
|
||||
* added text label support
|
||||
* general optimizations
|
||||
-> more bit shifting/operations, less Math.floors
|
||||
|
||||
Will either be merged into node-drawille or become an own module at some point
|
||||
###
|
||||
|
||||
module.exports = class BrailleBuffer
|
||||
characterMap: [[0x1, 0x8],[0x2, 0x10],[0x4, 0x20],[0x40, 0x80]]
|
||||
|
||||
pixelBuffer: null
|
||||
charBuffer: null
|
||||
foregroundBuffer: null
|
||||
backgroundBuffer: null
|
||||
|
||||
globalBackground: null
|
||||
|
||||
termReset: "\x1B[39;49m"
|
||||
|
||||
constructor: (@width, @height) ->
|
||||
size = @width*@height/8
|
||||
@pixelBuffer = new Buffer size
|
||||
@foregroundBuffer = new Buffer size
|
||||
@backgroundBuffer = new Buffer size
|
||||
@clear()
|
||||
|
||||
clear: ->
|
||||
@pixelBuffer.fill 0
|
||||
@charBuffer = []
|
||||
@foregroundBuffer.fill 0
|
||||
@backgroundBuffer.fill 0
|
||||
|
||||
setGlobalBackground: (@globalBackground) ->
|
||||
|
||||
setBackground: (x, y, color) ->
|
||||
return unless 0 <= x < @width and 0 <= y < @height
|
||||
idx = @_project x, y
|
||||
@backgroundBuffer[idx] = color
|
||||
|
||||
setPixel: (x, y, color) ->
|
||||
@_locate x, y, (idx, mask) =>
|
||||
@pixelBuffer[idx] |= mask
|
||||
@foregroundBuffer[idx] = color
|
||||
|
||||
unsetPixel: (x, y) ->
|
||||
@_locate x, y, (idx, mask) =>
|
||||
@pixelBuffer[idx] &= ~mask
|
||||
|
||||
_project: (x, y) ->
|
||||
(x>>1) + (@width>>1)*(y>>2)
|
||||
|
||||
_locate: (x, y, cb) ->
|
||||
return unless 0 <= x < @width and 0 <= y < @height
|
||||
idx = @_project x, y
|
||||
mask = @characterMap[y&3][x&1]
|
||||
cb idx, mask
|
||||
|
||||
_termColor: (foreground, background) ->
|
||||
background = background or @globalBackground
|
||||
if foreground and background
|
||||
"\x1B[38;5;#{foreground};48;5;#{background}m"
|
||||
else if foreground
|
||||
"\x1B[49;38;5;#{foreground}m"
|
||||
else if background
|
||||
"\x1B[39;48;5;#{background}m"
|
||||
else
|
||||
@termReset
|
||||
|
||||
frame: ->
|
||||
output = []
|
||||
currentColor = null
|
||||
delimeter = "\n"
|
||||
|
||||
for idx in [0...@pixelBuffer.length]
|
||||
output.push delimeter if idx and (idx % (@width/2)) is 0
|
||||
|
||||
if currentColor isnt colorCode = @_termColor @foregroundBuffer[idx], @backgroundBuffer[idx]
|
||||
output.push currentColor = colorCode
|
||||
|
||||
output.push if @charBuffer[idx]
|
||||
@charBuffer[idx]
|
||||
else
|
||||
String.fromCharCode 0x2800+@pixelBuffer[idx]
|
||||
|
||||
output.push @termReset+delimeter
|
||||
output.join ''
|
||||
|
||||
setChar: (char, x, y, color) ->
|
||||
return unless 0 <= x < @width and 0 <= y < @height
|
||||
idx = @_project x, y
|
||||
@charBuffer[idx] = char
|
||||
@foregroundBuffer[idx] = color
|
||||
|
||||
writeText: (text, x, y, color, center = true) ->
|
||||
x -= text.length/2+1 if center
|
||||
@setChar text.charAt(i), x+i*2, y, color for i in [0...text.length]
|
|
@ -0,0 +1,211 @@
|
|||
/*
|
||||
termap - Terminal Map Viewer
|
||||
by Michael Strassburger <codepoet@cpan.org>
|
||||
|
||||
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;
|
|
@ -1,161 +0,0 @@
|
|||
###
|
||||
termap - Terminal Map Viewer
|
||||
by Michael Strassburger <codepoet@cpan.org>
|
||||
|
||||
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
|
||||
###
|
||||
|
||||
bresenham = require 'bresenham'
|
||||
simplify = require 'simplify-js'
|
||||
|
||||
earcut = require 'earcut'
|
||||
BrailleBuffer = require './BrailleBuffer'
|
||||
utils = require './utils'
|
||||
|
||||
module.exports = class Canvas
|
||||
stack: []
|
||||
|
||||
constructor: (@width, @height) ->
|
||||
@buffer = new BrailleBuffer @width, @height
|
||||
|
||||
frame: ->
|
||||
@buffer.frame()
|
||||
|
||||
clear: ->
|
||||
@buffer.clear()
|
||||
|
||||
text: (text, x, y, color, center = false) ->
|
||||
@buffer.writeText text, x, y, color, center
|
||||
|
||||
line: (from, to, color, width = 1) ->
|
||||
@_line from.x, from.y, to.x, to.y, color, width
|
||||
|
||||
polyline: (points, color, width = 1) ->
|
||||
for i in [1...points.length]
|
||||
@_line points[i-1].x, points[i-1].y, points[i].x, points[i].y, width, color
|
||||
|
||||
setBackground: (color) ->
|
||||
@buffer.setGlobalBackground color
|
||||
|
||||
background: (x, y, color) ->
|
||||
@buffer.setBackground x, y, color
|
||||
|
||||
polygon: (rings, color) ->
|
||||
vertices = []
|
||||
holes = []
|
||||
|
||||
for ring in rings
|
||||
if vertices.length
|
||||
continue if ring.length < 3
|
||||
holes.push vertices.length/2
|
||||
else
|
||||
return false if ring.length < 3
|
||||
|
||||
for point in ring
|
||||
vertices.push point.x
|
||||
vertices.push point.y
|
||||
|
||||
try
|
||||
triangles = earcut vertices, holes
|
||||
catch e
|
||||
return false
|
||||
|
||||
for i in [0...triangles.length] by 3
|
||||
pa = @_polygonExtract vertices, triangles[i]
|
||||
pb = @_polygonExtract vertices, triangles[i+1]
|
||||
pc = @_polygonExtract vertices, triangles[i+2]
|
||||
|
||||
@_filledTriangle pa, pb, pc, color
|
||||
|
||||
true
|
||||
|
||||
_polygonExtract: (vertices, pointId) ->
|
||||
[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
|
||||
unless width = Math.max 0, width-1
|
||||
return bresenham x0, y0, x1, y1,
|
||||
(x, y) => @buffer.setPixel x, y, color
|
||||
|
||||
dx = Math.abs x1-x0
|
||||
sx = if x0 < x1 then 1 else -1
|
||||
dy = Math.abs y1-y0
|
||||
sy = if y0 < y1 then 1 else -1
|
||||
|
||||
err = dx-dy
|
||||
|
||||
ed = if dx+dy is 0 then 1 else Math.sqrt dx*dx+dy*dy
|
||||
|
||||
width = (width+1)/2
|
||||
loop
|
||||
@buffer.setPixel x0, y0, color
|
||||
e2 = err
|
||||
x2 = x0
|
||||
|
||||
if 2*e2 >= -dx
|
||||
e2 += dy
|
||||
y2 = y0
|
||||
while e2 < ed*width && (y1 != y2 || dx > dy)
|
||||
@buffer.setPixel x0, y2 += sy, color
|
||||
e2 += dx
|
||||
break if x0 is x1
|
||||
e2 = err
|
||||
err -= dy
|
||||
x0 += sx
|
||||
|
||||
if 2*e2 <= dy
|
||||
e2 = dx-e2
|
||||
while e2 < ed*width && (x1 != x2 || dx < dy)
|
||||
@buffer.setPixel x2 += sx, y0, color
|
||||
e2 += dy
|
||||
break if y0 is y1
|
||||
err += dx
|
||||
y0 += sy
|
||||
|
||||
_filledRectangle: (x, y, width, height, color) ->
|
||||
pointA = [x, y]
|
||||
pointB = [x+width, y]
|
||||
pointC = [x, y+height]
|
||||
pointD = [x+width, y+height]
|
||||
|
||||
@_filledTriangle pointA, pointB, pointC, color
|
||||
@_filledTriangle pointC, pointB, pointD, color
|
||||
|
||||
_bresenham: (pointA, pointB) ->
|
||||
bresenham pointA[0], pointA[1],
|
||||
pointB[0], pointB[1]
|
||||
|
||||
# Draws a filled triangle
|
||||
_filledTriangle: (pointA, pointB, pointC, color) ->
|
||||
a = @_bresenham pointB, pointC
|
||||
b = @_bresenham pointA, pointC
|
||||
c = @_bresenham pointA, pointB
|
||||
|
||||
points = a.concat(b).concat(c)
|
||||
.filter (point) => 0 <= point.y < @height
|
||||
.sort (a, b) -> if a.y is b.y then a.x - b.x else a.y-b.y
|
||||
|
||||
for i in [0...points.length]
|
||||
point = points[i]
|
||||
next = points[i*1+1]
|
||||
|
||||
if point.y is next?.y
|
||||
left = Math.max 0, point.x
|
||||
right = Math.min @width-1, next.x
|
||||
if left >= 0 and right <= @width
|
||||
@buffer.setPixel x, point.y, color for x in [left..right]
|
||||
|
||||
else
|
||||
@buffer.setPixel point.x, point.y, color
|
||||
|
||||
break unless next
|
|
@ -0,0 +1,202 @@
|
|||
/*
|
||||
termap - Terminal Map Viewer
|
||||
by Michael Strassburger <codepoet@cpan.org>
|
||||
|
||||
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;
|
|
@ -1,45 +0,0 @@
|
|||
###
|
||||
termap - Terminal Map Viewer
|
||||
by Michael Strassburger <codepoet@cpan.org>
|
||||
|
||||
Using 2D spatial indexing to avoid overlapping labels and markers
|
||||
and to find labels underneath a mouse cursor's position
|
||||
###
|
||||
|
||||
rbush = require 'rbush'
|
||||
|
||||
module.exports = class LabelBuffer
|
||||
tree: null
|
||||
|
||||
margin: 5
|
||||
|
||||
constructor: (@width, @height) ->
|
||||
@tree = rbush()
|
||||
|
||||
clear: ->
|
||||
@tree.clear()
|
||||
|
||||
project: (x, y) ->
|
||||
[Math.floor(x/2), Math.floor(y/4)]
|
||||
|
||||
writeIfPossible: (text, x, y, feature, margin = @margin) ->
|
||||
point = @project x, y
|
||||
|
||||
if @_hasSpace text, point[0], point[1]
|
||||
data = @_calculateArea text, point[0], point[1], margin
|
||||
data.feature = feature
|
||||
@tree.insert data
|
||||
else
|
||||
false
|
||||
|
||||
featuresAt: (x, y) ->
|
||||
@tree.search minX: x, maxX: x, minY: y, maxY: y
|
||||
|
||||
_hasSpace: (text, x, y) ->
|
||||
not @tree.collides @_calculateArea text, x, y
|
||||
|
||||
_calculateArea: (text, x, y, margin = 0) ->
|
||||
minX: x-margin
|
||||
minY: y-margin/2
|
||||
maxX: x+margin+text.length
|
||||
maxY: y+margin/2
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
termap - Terminal Map Viewer
|
||||
by Michael Strassburger <codepoet@cpan.org>
|
||||
|
||||
Using 2D spatial indexing to avoid overlapping labels and markers
|
||||
and to find labels underneath a mouse cursor's position
|
||||
*/
|
||||
'use strict';
|
||||
const RBush = require('rbush');
|
||||
const stringWidth = require('string-width');
|
||||
|
||||
module.exports = class LabelBuffer {
|
||||
|
||||
constructor() {
|
||||
this.tree = new RBush();
|
||||
this.margin = 5;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.tree.clear();
|
||||
}
|
||||
|
||||
project(x, y) {
|
||||
return [Math.floor(x/2), Math.floor(y/4)];
|
||||
}
|
||||
|
||||
writeIfPossible(text, x, y, feature, margin) {
|
||||
margin = margin || this.margin;
|
||||
|
||||
const point = this.project(x, y);
|
||||
|
||||
if (this._hasSpace(text, point[0], point[1])) {
|
||||
const data = this._calculateArea(text, point[0], point[1], margin);
|
||||
data.feature = feature;
|
||||
return this.tree.insert(data);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
featuresAt(x, y) {
|
||||
this.tree.search({minX: x, maxX: x, minY: y, maxY: y});
|
||||
}
|
||||
|
||||
_hasSpace(text, x, y) {
|
||||
return !this.tree.collides(this._calculateArea(text, x, y));
|
||||
}
|
||||
|
||||
_calculateArea(text, x, y, margin = 0) {
|
||||
return {
|
||||
minX: x-margin,
|
||||
minY: y-margin/2,
|
||||
maxX: x+margin+stringWidth(text),
|
||||
maxY: y+margin/2,
|
||||
};
|
||||
}
|
||||
};
|
|
@ -1,228 +0,0 @@
|
|||
###
|
||||
mapscii - Terminal Map Viewer
|
||||
by Michael Strassburger <codepoet@cpan.org>
|
||||
|
||||
UI and central command center
|
||||
###
|
||||
|
||||
keypress = require 'keypress'
|
||||
TermMouse = require 'term-mouse'
|
||||
Promise = require 'bluebird'
|
||||
|
||||
Renderer = require './Renderer'
|
||||
TileSource = require './TileSource'
|
||||
utils = require './utils'
|
||||
config = require './config'
|
||||
|
||||
module.exports = class Mapscii
|
||||
width: null
|
||||
height: null
|
||||
canvas: null
|
||||
mouse: null
|
||||
|
||||
mouseDragging: false
|
||||
mousePosition:
|
||||
x: 0, y: 0
|
||||
|
||||
tileSource: null
|
||||
renderer: null
|
||||
|
||||
zoom: 0
|
||||
center:
|
||||
# sf lat: 37.787946, lon: -122.407522
|
||||
# iceland lat: 64.124229, lon: -21.811552
|
||||
# rgbg
|
||||
# lat: 49.019493, lon: 12.098341
|
||||
lat: 52.51298, lon: 13.42012
|
||||
|
||||
minZoom: null
|
||||
|
||||
constructor: (options) ->
|
||||
config[key] = val for key, val of options
|
||||
|
||||
init: ->
|
||||
Promise
|
||||
.resolve()
|
||||
.then =>
|
||||
unless config.headless
|
||||
@_initKeyboard()
|
||||
@_initMouse()
|
||||
|
||||
@_initTileSource()
|
||||
|
||||
.then =>
|
||||
@_initRenderer()
|
||||
|
||||
.then =>
|
||||
@_draw()
|
||||
|
||||
_initTileSource: ->
|
||||
@tileSource = new TileSource()
|
||||
@tileSource.init config.source
|
||||
|
||||
_initKeyboard: ->
|
||||
keypress config.input
|
||||
config.input.setRawMode true
|
||||
config.input.resume()
|
||||
|
||||
config.input.on 'keypress', (ch, key) => @_onKey key
|
||||
|
||||
_initMouse: ->
|
||||
@mouse = TermMouse input: config.input, output: config.output
|
||||
@mouse.start()
|
||||
|
||||
@mouse.on 'click', (event) => @_onClick event
|
||||
@mouse.on 'scroll', (event) => @_onMouseScroll event
|
||||
@mouse.on 'move', (event) => @_onMouseMove event
|
||||
|
||||
_initRenderer: ->
|
||||
@renderer = new Renderer config.output, @tileSource
|
||||
@renderer.loadStyleFile config.styleFile
|
||||
|
||||
config.output.on 'resize', =>
|
||||
@_resizeRenderer()
|
||||
@_draw()
|
||||
|
||||
@_resizeRenderer()
|
||||
@zoom = if config.initialZoom isnt null then config.initialZoom else @minZoom
|
||||
|
||||
_resizeRenderer: (cb) ->
|
||||
if config.size
|
||||
@width = config.size.width
|
||||
@height = config.size.height
|
||||
else
|
||||
@width = config.output.columns >> 1 << 2
|
||||
@height = config.output.rows * 4 - 4
|
||||
|
||||
@minZoom = 4-Math.log(4096/@width)/Math.LN2
|
||||
|
||||
@renderer.setSize @width, @height
|
||||
|
||||
_updateMousePosition: (event) ->
|
||||
projected =
|
||||
x: (event.x-.5)*2
|
||||
y: (event.y-.5)*4
|
||||
|
||||
size = utils.tilesizeAtZoom @zoom
|
||||
[dx, dy] = [projected.x-@width/2, projected.y-@height/2]
|
||||
|
||||
z = utils.baseZoom @zoom
|
||||
center = utils.ll2tile @center.lon, @center.lat, z
|
||||
@mousePosition = utils.tile2ll center.x+(dx/size), center.y+(dy/size), z
|
||||
|
||||
_onClick: (event) ->
|
||||
@_updateMousePosition event
|
||||
|
||||
if @mouseDragging and event.button is "left"
|
||||
@mouseDragging = false
|
||||
else
|
||||
@setCenter @mousePosition.lat, @mousePosition.lon
|
||||
|
||||
@_draw()
|
||||
|
||||
_onMouseScroll: (event) ->
|
||||
@_updateMousePosition event
|
||||
# TODO: handle .x/y for directed zoom
|
||||
@zoomBy config.zoomStep * if event.button is "up" then 1 else -1
|
||||
@_draw()
|
||||
|
||||
_onMouseMove: (event) ->
|
||||
# start dragging
|
||||
if event.button is "left"
|
||||
if @mouseDragging
|
||||
dx = (@mouseDragging.x-event.x)*2
|
||||
dy = (@mouseDragging.y-event.y)*4
|
||||
|
||||
size = utils.tilesizeAtZoom @zoom
|
||||
|
||||
newCenter = utils.tile2ll @mouseDragging.center.x+(dx/size),
|
||||
@mouseDragging.center.y+(dy/size),
|
||||
utils.baseZoom(@zoom)
|
||||
|
||||
@setCenter newCenter.lat, newCenter.lon
|
||||
|
||||
@_draw()
|
||||
|
||||
else
|
||||
@mouseDragging =
|
||||
x: event.x,
|
||||
y: event.y,
|
||||
center: utils.ll2tile @center.lon, @center.lat, utils.baseZoom(@zoom)
|
||||
|
||||
@_updateMousePosition event
|
||||
@notify @_getFooter()
|
||||
|
||||
_onKey: (key) ->
|
||||
# check if the pressed key is configured
|
||||
draw = switch key?.name
|
||||
when "q"
|
||||
process.exit 0
|
||||
|
||||
when "w" then @zoomy = 1
|
||||
when "s" then @zoomy = -1
|
||||
|
||||
when "a" then @zoomBy config.zoomStep
|
||||
when "z" then @zoomBy -config.zoomStep
|
||||
|
||||
when "left" then @moveBy 0, -8/Math.pow(2, @zoom)
|
||||
when "right" then @moveBy 0, 8/Math.pow(2, @zoom)
|
||||
when "up" then @moveBy 6/Math.pow(2, @zoom), 0
|
||||
when "down" then @moveBy -6/Math.pow(2, @zoom), 0
|
||||
|
||||
else
|
||||
null
|
||||
|
||||
if draw isnt null
|
||||
@_draw()
|
||||
else
|
||||
# display debug info for unhandled keys
|
||||
@notify JSON.stringify key
|
||||
|
||||
_draw: ->
|
||||
@renderer
|
||||
.draw @center, @zoom
|
||||
.then (frame) =>
|
||||
@_write frame
|
||||
@notify @_getFooter()
|
||||
.catch =>
|
||||
@notify "renderer is busy"
|
||||
.then =>
|
||||
if @zoomy
|
||||
if (@zoomy > 0 and @zoom < config.maxZoom) or (@zoomy < 0 and @zoom > @minZoom)
|
||||
@zoom += @zoomy * config.zoomStep
|
||||
else
|
||||
@zoomy *= -1
|
||||
setImmediate => @_draw()
|
||||
|
||||
_getFooter: ->
|
||||
# tile = utils.ll2tile @center.lon, @center.lat, @zoom
|
||||
# "tile: #{utils.digits tile.x, 3}, #{utils.digits tile.x, 3} "+
|
||||
|
||||
"center: #{utils.digits @center.lat, 3}, #{utils.digits @center.lon, 3} "+
|
||||
"zoom: #{utils.digits @zoom, 2} "+
|
||||
"mouse: #{utils.digits @mousePosition.lat, 3}, #{utils.digits @mousePosition.lon, 3} "
|
||||
|
||||
notify: (text) ->
|
||||
@_write "\r\x1B[K"+text unless config.headless
|
||||
|
||||
_write: (output) ->
|
||||
config.output.write output
|
||||
|
||||
zoomBy: (step) ->
|
||||
return @zoom = @minZoom if @zoom+step < @minZoom
|
||||
return @zoom = config.maxZoom if @zoom+step > config.maxZoom
|
||||
|
||||
@zoom += step
|
||||
|
||||
moveBy: (lat, lon) ->
|
||||
@setCenter @center.lat+lat, @center.lon+lon
|
||||
|
||||
setCenter: (lat, lon) ->
|
||||
lon += 360 if lon < -180
|
||||
lon -= 360 if lon > 180
|
||||
|
||||
lat = 85.0511 if lat > 85.0511
|
||||
lat = -85.0511 if lat < -85.0511
|
||||
|
||||
@center.lat = lat
|
||||
@center.lon = lon
|
|
@ -0,0 +1,313 @@
|
|||
/*
|
||||
MapSCII - Terminal Map Viewer
|
||||
by Michael Strassburger <codepoet@cpan.org>
|
||||
|
||||
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;
|
|
@ -1,273 +0,0 @@
|
|||
###
|
||||
termap - Terminal Map Viewer
|
||||
by Michael Strassburger <codepoet@cpan.org>
|
||||
|
||||
The Console Vector Tile renderer - bäm!
|
||||
###
|
||||
Promise = require 'bluebird'
|
||||
x256 = require 'x256'
|
||||
simplify = require 'simplify-js'
|
||||
|
||||
Canvas = require './Canvas'
|
||||
LabelBuffer = require './LabelBuffer'
|
||||
Styler = require './Styler'
|
||||
Tile = require './Tile'
|
||||
utils = require './utils'
|
||||
config = require './config'
|
||||
|
||||
module.exports = class Renderer
|
||||
terminal:
|
||||
CLEAR: "\x1B[2J"
|
||||
MOVE: "\x1B[?6h"
|
||||
|
||||
isDrawing: false
|
||||
lastDrawAt: 0
|
||||
|
||||
labelBuffer: null
|
||||
tileSource: null
|
||||
tilePadding: 64
|
||||
|
||||
constructor: (@output, @tileSource) ->
|
||||
@labelBuffer = new LabelBuffer()
|
||||
|
||||
loadStyleFile: (file) ->
|
||||
@styler = new Styler file
|
||||
@tileSource.useStyler @styler
|
||||
|
||||
setSize: (@width, @height) ->
|
||||
@canvas = new Canvas @width, @height
|
||||
|
||||
draw: (center, zoom) ->
|
||||
return Promise.reject() if @isDrawing
|
||||
@isDrawing = true
|
||||
|
||||
@labelBuffer.clear()
|
||||
@_seen = {}
|
||||
|
||||
if color = @styler.styleById['background']?.paint['background-color']
|
||||
@canvas.setBackground x256 utils.hex2rgb color
|
||||
|
||||
@canvas.clear()
|
||||
|
||||
Promise
|
||||
.resolve @_visibleTiles center, zoom
|
||||
.map (tile) => @_getTile tile
|
||||
.map (tile) => @_getTileFeatures tile, zoom
|
||||
.then (tiles) => @_renderTiles tiles
|
||||
.then => @_getFrame()
|
||||
|
||||
.catch (e) ->
|
||||
console.log e
|
||||
|
||||
.finally (frame) =>
|
||||
@isDrawing = false
|
||||
@lastDrawAt = Date.now()
|
||||
|
||||
frame
|
||||
|
||||
_visibleTiles: (center, zoom) ->
|
||||
z = utils.baseZoom zoom
|
||||
center = utils.ll2tile center.lon, center.lat, z
|
||||
|
||||
tiles = []
|
||||
tileSize = utils.tilesizeAtZoom zoom
|
||||
|
||||
for y in [Math.floor(center.y)-1..Math.floor(center.y)+1]
|
||||
for x in [Math.floor(center.x)-1..Math.floor(center.x)+1]
|
||||
tile = x: x, y: y, z: z
|
||||
|
||||
position =
|
||||
x: @width/2-(center.x-tile.x)*tileSize
|
||||
y: @height/2-(center.y-tile.y)*tileSize
|
||||
|
||||
gridSize = Math.pow 2, z
|
||||
|
||||
tile.x %= gridSize
|
||||
if tile.x < 0
|
||||
tile.x = if z is 0 then 0 else tile.x+gridSize
|
||||
|
||||
if tile.y < 0 or
|
||||
tile.y >= gridSize or
|
||||
position.x+tileSize < 0 or
|
||||
position.y+tileSize < 0 or
|
||||
position.x>@width or
|
||||
position.y>@height
|
||||
continue
|
||||
|
||||
tiles.push xyz: tile, zoom: zoom, position: position, size: tileSize
|
||||
|
||||
tiles
|
||||
|
||||
_getTile: (tile) ->
|
||||
@tileSource
|
||||
.getTile tile.xyz.z, tile.xyz.x, tile.xyz.y
|
||||
.then (data) =>
|
||||
tile.data = data
|
||||
tile
|
||||
|
||||
_getTileFeatures: (tile, zoom) ->
|
||||
position = tile.position
|
||||
layers = {}
|
||||
|
||||
for layerId in @_generateDrawOrder zoom
|
||||
continue unless layer = tile.data.layers?[layerId]
|
||||
|
||||
scale = layer.extent / utils.tilesizeAtZoom zoom
|
||||
layers[layerId] =
|
||||
scale: scale
|
||||
features: layer.tree.search
|
||||
minX: -position.x*scale
|
||||
minY: -position.y*scale
|
||||
maxX: (@width-position.x)*scale
|
||||
maxY: (@height-position.y)*scale
|
||||
|
||||
tile.layers = layers
|
||||
tile
|
||||
|
||||
_renderTiles: (tiles) ->
|
||||
drawn = {}
|
||||
labels = []
|
||||
|
||||
for layerId in @_generateDrawOrder tiles[0].xyz.z
|
||||
for tile in tiles
|
||||
continue unless layer = tile.layers[layerId]
|
||||
for feature in layer.features
|
||||
# continue if feature.id and drawn[feature.id]
|
||||
# drawn[feature.id] = true
|
||||
if layerId.match /label/
|
||||
labels.push tile: tile, feature: feature, scale: layer.scale
|
||||
else
|
||||
@_drawFeature tile, feature, layer.scale
|
||||
|
||||
labels.sort (a, b) ->
|
||||
if a.feature.properties.localrank
|
||||
a.feature.properties.localrank-b.feature.properties.localrank
|
||||
else
|
||||
a.feature.properties.scalerank-b.feature.properties.scalerank
|
||||
|
||||
for label in labels
|
||||
@_drawFeature label.tile, label.feature, label.scale
|
||||
|
||||
_getFrame: ->
|
||||
frame = ""
|
||||
frame += @terminal.CLEAR unless @lastDrawAt
|
||||
frame += @terminal.MOVE
|
||||
frame += @canvas.frame()
|
||||
frame
|
||||
|
||||
featuresAt: (x, y) ->
|
||||
@labelBuffer.featuresAt x, y
|
||||
|
||||
_drawFeature: (tile, feature, scale) ->
|
||||
if feature.style.minzoom and tile.zoom < feature.style.minzoom
|
||||
return false
|
||||
else if feature.style.maxzoom and tile.zoom > feature.style.maxzoom
|
||||
return false
|
||||
|
||||
switch feature.style.type
|
||||
when "line"
|
||||
width = feature.style.paint['line-width']
|
||||
# TODO: apply the correct zoom based value
|
||||
width = width.stops[0][1] if width instanceof Object
|
||||
|
||||
points = @_scaleAndReduce tile, feature, feature.points, scale
|
||||
@canvas.polyline points, feature.color, width if points.length
|
||||
|
||||
when "fill"
|
||||
points = (@_scaleAndReduce tile, feature, p, scale, false for p in feature.points)
|
||||
@canvas.polygon points, feature.color
|
||||
|
||||
when "symbol"
|
||||
text = feature.properties["name_"+config.language] or
|
||||
feature.properties["name_en"] or
|
||||
feature.properties["name"] or
|
||||
feature.properties.house_num or
|
||||
genericSymbol = "◉"
|
||||
|
||||
return false if @_seen[text] and not genericSymbol
|
||||
|
||||
placed = false
|
||||
for point in @_scaleAndReduce tile, feature, feature.points, scale
|
||||
x = point.x - text.length
|
||||
margin = config.layers[feature.layer]?.margin or config.labelMargin
|
||||
|
||||
if @labelBuffer.writeIfPossible text, x, point.y, feature, margin
|
||||
@canvas.text text, x, point.y, feature.color
|
||||
placed = true
|
||||
break
|
||||
|
||||
else if config.layers[feature.layer]?.cluster and
|
||||
@labelBuffer.writeIfPossible "◉", point.x, point.y, feature, 3
|
||||
@canvas.text "◉", point.x, point.y, feature.color
|
||||
placed = true
|
||||
break
|
||||
|
||||
@_seen[text] = true if placed
|
||||
|
||||
true
|
||||
|
||||
_scaleAndReduce: (tile, feature, points, scale, filter = true) ->
|
||||
lastX = lastY = outside = null
|
||||
scaled = []
|
||||
|
||||
minX = minY = -@tilePadding
|
||||
maxX = @width+@tilePadding
|
||||
maxY = @height+@tilePadding
|
||||
|
||||
for point in points
|
||||
x = Math.floor tile.position.x+(point.x/scale)
|
||||
y = Math.floor tile.position.y+(point.y/scale)
|
||||
|
||||
continue if lastX is x and lastY is y
|
||||
|
||||
lastY = y
|
||||
lastX = x
|
||||
|
||||
if filter
|
||||
if x < minX or x > maxX or y < minY or y > maxY
|
||||
continue if outside
|
||||
outside = true
|
||||
else
|
||||
if outside
|
||||
outside = null
|
||||
scaled.push x: lastX, y: lastY
|
||||
|
||||
scaled.push x: x, y: y
|
||||
|
||||
if feature.style.type isnt "symbol"
|
||||
if scaled.length < 2
|
||||
return []
|
||||
|
||||
if config.simplifyPolylines
|
||||
simplify scaled, .5, true
|
||||
else
|
||||
scaled
|
||||
else
|
||||
scaled
|
||||
|
||||
_generateDrawOrder: (zoom) ->
|
||||
if zoom < 2
|
||||
[
|
||||
"admin"
|
||||
"water"
|
||||
"country_label"
|
||||
"marine_label"
|
||||
]
|
||||
else
|
||||
[
|
||||
"landuse"
|
||||
"water"
|
||||
"marine_label"
|
||||
"building"
|
||||
"road"
|
||||
"admin"
|
||||
|
||||
"country_label"
|
||||
"state_label"
|
||||
"water_label"
|
||||
"place_label"
|
||||
"rail_station_label"
|
||||
"poi_label"
|
||||
"road_label"
|
||||
"housenum_label"
|
||||
]
|
|
@ -0,0 +1,335 @@
|
|||
/*
|
||||
termap - Terminal Map Viewer
|
||||
by Michael Strassburger <codepoet@cpan.org>
|
||||
|
||||
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;
|
|
@ -1,112 +0,0 @@
|
|||
###
|
||||
termap - Terminal Map Viewer
|
||||
by Michael Strassburger <codepoet@cpan.org>
|
||||
|
||||
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.
|
||||
###
|
||||
|
||||
fs = require 'fs'
|
||||
|
||||
module.exports = class Styler
|
||||
styleById: {}
|
||||
styleByLayer: {}
|
||||
|
||||
constructor: (file) ->
|
||||
json = JSON.parse fs.readFileSync(file).toString()
|
||||
@styleName = json.name
|
||||
|
||||
@_replaceConstants json.constants, json.layers if json.constants
|
||||
|
||||
for style in json.layers
|
||||
if style.ref and @styleById[style.ref]
|
||||
for ref in ['type', 'source-layer', 'minzoom', 'maxzoom', 'filter']
|
||||
if @styleById[style.ref][ref] and not style[ref]
|
||||
style[ref] = @styleById[style.ref][ref]
|
||||
|
||||
style.appliesTo = @_compileFilter style.filter
|
||||
|
||||
@styleByLayer[style['source-layer']] ?= []
|
||||
@styleByLayer[style['source-layer']].push style
|
||||
@styleById[style.id] = style
|
||||
|
||||
getStyleFor: (layer, feature, zoom) ->
|
||||
return false unless @styleByLayer[layer]
|
||||
|
||||
for style in @styleByLayer[layer]
|
||||
if style.appliesTo feature
|
||||
return style
|
||||
|
||||
return false
|
||||
|
||||
_replaceConstants: (constants, tree) ->
|
||||
for id, node of tree
|
||||
switch typeof node
|
||||
when 'object'
|
||||
continue if node.constructor.name.match /Stream/
|
||||
@_replaceConstants constants, node
|
||||
|
||||
when 'string'
|
||||
if node.charAt(0) is '@'
|
||||
tree[id] = constants[node]
|
||||
null
|
||||
|
||||
_compileFilter: (filter) ->
|
||||
switch filter?[0]
|
||||
when "all"
|
||||
filters = (@_compileFilter subFilter for subFilter in filter[1..])
|
||||
(feature) ->
|
||||
return false for appliesTo in filters when not appliesTo feature
|
||||
true
|
||||
|
||||
when "any"
|
||||
filters = (@_compileFilter subFilter for subFilter in filter[1..])
|
||||
(feature) ->
|
||||
return true for appliesTo in filters when appliesTo feature
|
||||
false
|
||||
|
||||
when "none"
|
||||
filters = (@_compileFilter subFilter for subFilter in filter[1..])
|
||||
(feature) ->
|
||||
return false for appliesTo in filters when appliesTo feature
|
||||
true
|
||||
|
||||
when "=="
|
||||
(feature) -> feature.properties[filter[1]] is filter[2]
|
||||
|
||||
when "!="
|
||||
(feature) -> feature.properties[filter[1]] isnt filter[2]
|
||||
|
||||
when "in"
|
||||
(feature) ->
|
||||
return true for value in filter[2..] when feature.properties[filter[1]] is value
|
||||
false
|
||||
|
||||
when "!in"
|
||||
(feature) ->
|
||||
return false for value in filter[2..] when feature.properties[filter[1]] is value
|
||||
true
|
||||
|
||||
when "has"
|
||||
(feature) -> !!feature.properties[filter[1]]
|
||||
|
||||
when "!has"
|
||||
(feature) -> !feature.properties[filter[1]]
|
||||
|
||||
when ">"
|
||||
(feature) -> feature.properties[filter[1]] > filter[2]
|
||||
|
||||
when ">="
|
||||
(feature) -> feature.properties[filter[1]] >= filter[2]
|
||||
|
||||
when "<"
|
||||
(feature) -> feature.properties[filter[1]] < filter[2]
|
||||
|
||||
when "<="
|
||||
(feature) -> feature.properties[filter[1]] <= filter[2]
|
||||
|
||||
else
|
||||
-> true
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
termap - Terminal Map Viewer
|
||||
by Michael Strassburger <codepoet@cpan.org>
|
||||
|
||||
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;
|
140
src/Tile.coffee
140
src/Tile.coffee
|
@ -1,140 +0,0 @@
|
|||
###
|
||||
termap - Terminal Map Viewer
|
||||
by Michael Strassburger <codepoet@cpan.org>
|
||||
|
||||
Handling of and access to single VectorTiles
|
||||
###
|
||||
|
||||
VectorTile = require('vector-tile').VectorTile
|
||||
Protobuf = require 'pbf'
|
||||
Promise = require 'bluebird'
|
||||
zlib = require 'zlib'
|
||||
rbush = require 'rbush'
|
||||
x256 = require 'x256'
|
||||
earcut = require 'earcut'
|
||||
|
||||
utils = require "./utils"
|
||||
|
||||
class Tile
|
||||
layers: {}
|
||||
|
||||
constructor: (@styler) ->
|
||||
|
||||
load: (buffer) ->
|
||||
@_unzipIfNeeded buffer
|
||||
.then (buffer) => @_loadTile buffer
|
||||
.then => @_loadLayers()
|
||||
.then => this
|
||||
|
||||
_loadTile: (buffer) ->
|
||||
@tile = new VectorTile new Protobuf buffer
|
||||
|
||||
_unzipIfNeeded: (buffer) ->
|
||||
new Promise (resolve, reject) =>
|
||||
if @_isGzipped buffer
|
||||
zlib.gunzip buffer, (err, data) ->
|
||||
return reject err if err
|
||||
resolve data
|
||||
else
|
||||
resolve buffer
|
||||
|
||||
_isGzipped: (buffer) ->
|
||||
buffer.slice(0,2).indexOf(Buffer.from([0x1f, 0x8b])) is 0
|
||||
|
||||
_loadLayers: () ->
|
||||
layers = {}
|
||||
colorCache = {}
|
||||
|
||||
for name, layer of @tile.layers
|
||||
nodes = []
|
||||
#continue if name is "water"
|
||||
for i in [0...layer.length]
|
||||
# TODO: caching of similar attributes to avoid looking up the style each time
|
||||
#continue if @styler and not @styler.getStyleFor layer, feature
|
||||
|
||||
feature = layer.feature i
|
||||
feature.properties.$type = type = [undefined, "Point", "LineString", "Polygon"][feature.type]
|
||||
|
||||
if @styler
|
||||
style = @styler.getStyleFor name, feature
|
||||
continue unless style
|
||||
|
||||
color =
|
||||
style.paint['line-color'] or
|
||||
style.paint['fill-color'] or
|
||||
style.paint['text-color']
|
||||
|
||||
# TODO: style zoom stops handling
|
||||
if color instanceof Object
|
||||
color = color.stops[0][1]
|
||||
|
||||
colorCode = colorCache[color] or 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
|
||||
geometries = feature.loadGeometry() #@_reduceGeometry feature, 8
|
||||
|
||||
if style.type is "fill"
|
||||
nodes.push @_addBoundaries true,
|
||||
id: feature.id
|
||||
layer: name
|
||||
style: style
|
||||
properties: feature.properties
|
||||
points: geometries
|
||||
color: colorCode
|
||||
|
||||
else
|
||||
for points in geometries
|
||||
nodes.push @_addBoundaries false,
|
||||
id: feature.id
|
||||
layer: name
|
||||
style: style
|
||||
properties: feature.properties
|
||||
points: points
|
||||
color: colorCode
|
||||
|
||||
|
||||
tree = rbush 18
|
||||
tree.load nodes
|
||||
|
||||
layers[name] =
|
||||
extent: layer.extent
|
||||
tree: tree
|
||||
|
||||
@layers = layers
|
||||
|
||||
_addBoundaries: (deep, data) ->
|
||||
minX = Infinity
|
||||
maxX = -Infinity
|
||||
minY = Infinity
|
||||
maxY = -Infinity
|
||||
|
||||
for p in (if deep then data.points[0] else data.points)
|
||||
minX = p.x if p.x < minX
|
||||
maxX = p.x if p.x > maxX
|
||||
minY = p.y if p.y < minY
|
||||
maxY = p.y if p.y > maxY
|
||||
|
||||
data.minX = minX
|
||||
data.maxX = maxX
|
||||
data.minY = minY
|
||||
data.maxY = maxY
|
||||
data
|
||||
|
||||
_reduceGeometry: (feature, factor) ->
|
||||
for points, i in feature.loadGeometry()
|
||||
reduced = []
|
||||
last = null
|
||||
for point in points
|
||||
p =
|
||||
x: Math.floor point.x/factor
|
||||
y: Math.floor point.y/factor
|
||||
|
||||
if last and last.x is p.x and last.y is p.y
|
||||
continue
|
||||
|
||||
reduced.push last = p
|
||||
|
||||
reduced
|
||||
|
||||
module.exports = Tile
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
termap - Terminal Map Viewer
|
||||
by Michael Strassburger <codepoet@cpan.org>
|
||||
|
||||
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;
|
|
@ -1,123 +0,0 @@
|
|||
###
|
||||
termap - Terminal Map Viewer
|
||||
by Michael Strassburger <codepoet@cpan.org>
|
||||
|
||||
Source for VectorTiles - supports
|
||||
* remote TileServer
|
||||
* local MBTiles and VectorTiles
|
||||
###
|
||||
|
||||
Promise = require 'bluebird'
|
||||
userhome = require 'userhome'
|
||||
fetch = require 'node-fetch'
|
||||
fs = require 'fs'
|
||||
|
||||
Tile = require './Tile'
|
||||
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 mbtiles
|
||||
MBTiles = try
|
||||
require 'mbtiles'
|
||||
catch
|
||||
null
|
||||
|
||||
module.exports = class TileSource
|
||||
cache: {}
|
||||
modes:
|
||||
MBTiles: 1
|
||||
VectorTile: 2
|
||||
HTTP: 3
|
||||
|
||||
mode: null
|
||||
mbtiles: null
|
||||
styler: null
|
||||
|
||||
init: (@source) ->
|
||||
if @source.startsWith "http"
|
||||
@_initPersistence() if config.persistDownloadedTiles
|
||||
|
||||
@mode = @modes.HTTP
|
||||
|
||||
else if @source.endsWith ".mbtiles"
|
||||
unless MBTiles
|
||||
throw new Error "MBTiles support must be installed with following command: 'npm install -g mbtiles'"
|
||||
|
||||
@mode = @modes.MBTiles
|
||||
@loadMBtils source
|
||||
|
||||
else
|
||||
throw new Error "source type isn't supported yet"
|
||||
|
||||
loadMBtils: (source) ->
|
||||
new Promise (resolve, reject) =>
|
||||
new MBTiles source, (err, @mbtiles) =>
|
||||
if err then reject err
|
||||
else resolve()
|
||||
|
||||
useStyler: (@styler) ->
|
||||
|
||||
getTile: (z, x, y) ->
|
||||
unless @mode
|
||||
throw new Error "no TileSource defined"
|
||||
|
||||
z = Math.max 0, Math.floor z
|
||||
|
||||
if cached = @cache[[z,x,y].join("-")]
|
||||
return Promise.resolve cached
|
||||
|
||||
switch @mode
|
||||
when @modes.MBTiles then @_getMBTile z, x, y
|
||||
when @modes.HTTP then @_getHTTP z, x, y
|
||||
|
||||
_getHTTP: (z, x, y) ->
|
||||
promise =
|
||||
if config.persistDownloadedTiles and tile = @_getPersited z, x, y
|
||||
Promise.resolve tile
|
||||
else
|
||||
fetch @source+[z,x,y].join("/")+".pbf"
|
||||
.then (res) => res.buffer()
|
||||
.then (buffer) =>
|
||||
@_persistTile z, x, y, buffer if config.persistDownloadedTiles
|
||||
buffer
|
||||
|
||||
promise
|
||||
.then (buffer) =>
|
||||
@_createTile z, x, y, buffer
|
||||
|
||||
_getMBTile: (z, x, y) ->
|
||||
new Promise (resolve, reject) =>
|
||||
@mbtiles.getTile z, x, y, (err, buffer) =>
|
||||
return reject err if err
|
||||
resolve @_createTile z, x, y, buffer
|
||||
|
||||
_createTile: (z, x, y, buffer) ->
|
||||
tile = @cache[[z,x,y].join("-")] = new Tile @styler
|
||||
tile.load buffer
|
||||
|
||||
_initPersistence: ->
|
||||
try
|
||||
@_createFolder userhome ".mapscii"
|
||||
@_createFolder userhome ".mapscii", "cache"
|
||||
catch error
|
||||
config.persistDownloadedTiles = false
|
||||
return
|
||||
|
||||
_persistTile: (z, x, y, buffer) ->
|
||||
zoom = z.toString()
|
||||
@_createFolder userhome ".mapscii", "cache", zoom
|
||||
fs.writeFile userhome(".mapscii", "cache", zoom, "#{x}-#{y}.pbf"), buffer, -> null
|
||||
|
||||
_getPersited: (z, x, y) ->
|
||||
try
|
||||
fs.readFileSync userhome ".mapscii", "cache", z.toString(), "#{x}-#{y}.pbf"
|
||||
catch error
|
||||
false
|
||||
|
||||
_createFolder: (path) ->
|
||||
try
|
||||
fs.mkdirSync path
|
||||
true
|
||||
catch e
|
||||
e.code is "EEXIST"
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
termap - Terminal Map Viewer
|
||||
by Michael Strassburger <codepoet@cpan.org>
|
||||
|
||||
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;
|
|
@ -0,0 +1,12 @@
|
|||
'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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,35 +0,0 @@
|
|||
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
|
||||
|
||||
simplifyPolylines: false
|
||||
|
||||
# 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
|
|
@ -0,0 +1,59 @@
|
|||
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: '◉',
|
||||
};
|
|
@ -1,59 +0,0 @@
|
|||
###
|
||||
termap - Terminal Map Viewer
|
||||
by Michael Strassburger <codepoet@cpan.org>
|
||||
|
||||
methods used all around
|
||||
###
|
||||
config = require './config'
|
||||
|
||||
constants =
|
||||
RADIUS: 6378137
|
||||
|
||||
utils =
|
||||
clamp: (num, min, max) ->
|
||||
if num <= min then min else if num >= max then max else num
|
||||
|
||||
baseZoom: (zoom) ->
|
||||
Math.min config.tileRange, Math.max 0, Math.floor zoom
|
||||
|
||||
tilesizeAtZoom: (zoom) ->
|
||||
config.projectSize * Math.pow(2, zoom-utils.baseZoom(zoom))
|
||||
|
||||
deg2rad: (angle) ->
|
||||
# (angle / 180) * Math.PI
|
||||
angle * 0.017453292519943295
|
||||
|
||||
ll2tile: (lon, lat, zoom) ->
|
||||
x: (lon+180)/360*Math.pow(2, zoom)
|
||||
y: (1-Math.log(Math.tan(lat*Math.PI/180)+1/Math.cos(lat*Math.PI/180))/Math.PI)/2*Math.pow(2, zoom)
|
||||
z: zoom
|
||||
|
||||
tile2ll: (x, y, zoom) ->
|
||||
n = Math.PI - 2*Math.PI*y/Math.pow(2, zoom)
|
||||
|
||||
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) ->
|
||||
(Math.cos(lat * Math.PI/180) * 2 * Math.PI * constants.RADIUS) / (256 * Math.pow(2, zoom))
|
||||
|
||||
hex2rgb: (color) ->
|
||||
return [255, 0, 0] unless color?.match
|
||||
|
||||
unless color.match /^#[a-fA-F0-9]{3,6}$/
|
||||
throw new Error "#{color} isn\'t a supported hex color"
|
||||
|
||||
color = color.substr 1
|
||||
decimal = parseInt color, 16
|
||||
|
||||
if color.length is 3
|
||||
rgb = [decimal>>8, (decimal>>4)&15, decimal&15]
|
||||
rgb.map (c) => c + (c<<4)
|
||||
else
|
||||
[(decimal>>16)&255, (decimal>>8)&255, decimal&255]
|
||||
|
||||
digits: (number, digits) ->
|
||||
Math.floor(number*Math.pow(10, digits))/Math.pow(10, digits)
|
||||
|
||||
|
||||
module.exports = utils
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
termap - Terminal Map Viewer
|
||||
by Michael Strassburger <codepoet@cpan.org>
|
||||
|
||||
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;
|
|
@ -0,0 +1,47 @@
|
|||
'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);
|
||||
});
|
||||
});
|
||||
});
|
Ładowanie…
Reference in New Issue