mapscii-py 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mapscii_py-0.1.0/LICENSE +23 -0
- mapscii_py-0.1.0/PKG-INFO +147 -0
- mapscii_py-0.1.0/README.md +101 -0
- mapscii_py-0.1.0/mapscii_py/__init__.py +15 -0
- mapscii_py-0.1.0/mapscii_py/__main__.py +6 -0
- mapscii_py-0.1.0/mapscii_py/canvas.py +179 -0
- mapscii_py-0.1.0/mapscii_py/cli.py +120 -0
- mapscii_py-0.1.0/mapscii_py/py.typed +1 -0
- mapscii_py-0.1.0/mapscii_py/renderer.py +164 -0
- mapscii_py-0.1.0/mapscii_py/rich_map.py +332 -0
- mapscii_py-0.1.0/mapscii_py/style.py +117 -0
- mapscii_py-0.1.0/mapscii_py/styles/bright.json +2191 -0
- mapscii_py-0.1.0/mapscii_py/styles/dark.json +1560 -0
- mapscii_py-0.1.0/mapscii_py/tiles.py +198 -0
- mapscii_py-0.1.0/mapscii_py/utils.py +78 -0
- mapscii_py-0.1.0/mapscii_py.egg-info/PKG-INFO +147 -0
- mapscii_py-0.1.0/mapscii_py.egg-info/SOURCES.txt +21 -0
- mapscii_py-0.1.0/mapscii_py.egg-info/dependency_links.txt +1 -0
- mapscii_py-0.1.0/mapscii_py.egg-info/entry_points.txt +2 -0
- mapscii_py-0.1.0/mapscii_py.egg-info/requires.txt +2 -0
- mapscii_py-0.1.0/mapscii_py.egg-info/top_level.txt +1 -0
- mapscii_py-0.1.0/pyproject.toml +42 -0
- mapscii_py-0.1.0/setup.cfg +4 -0
mapscii_py-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 Michael Straßburger
|
|
4
|
+
Copyright (c) 2019 The MapSCII authors
|
|
5
|
+
Copyright (c) 2026 David Nemeth-Csoka HA7NCS
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mapscii-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python port of MapSCII's vector-tile terminal renderer
|
|
5
|
+
Author-email: Dávid Németh-Csóka <encse@csokavar.hu>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2017 Michael Straßburger
|
|
9
|
+
Copyright (c) 2019 The MapSCII authors
|
|
10
|
+
Copyright (c) 2026 David Nemeth-Csoka HA7NCS
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
|
|
30
|
+
Project-URL: Source, https://github.com/encse/adbs-tui/tree/main/mapscii
|
|
31
|
+
Project-URL: Issues, https://github.com/encse/adbs-tui/issues
|
|
32
|
+
Keywords: mapscii,terminal,map,vector-tiles,rich
|
|
33
|
+
Classifier: Development Status :: 3 - Alpha
|
|
34
|
+
Classifier: Environment :: Console
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Operating System :: OS Independent
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
39
|
+
Classifier: Topic :: Terminals
|
|
40
|
+
Requires-Python: >=3.9
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
License-File: LICENSE
|
|
43
|
+
Requires-Dist: mapbox-vector-tile<3,>=2.2
|
|
44
|
+
Requires-Dist: rich<15,>=13
|
|
45
|
+
Dynamic: license-file
|
|
46
|
+
|
|
47
|
+
# mapscii-py
|
|
48
|
+
|
|
49
|
+
A Python port of [MapSCII](https://github.com/rastapasta/mapscii) for rendering
|
|
50
|
+
OpenStreetMap vector tiles in terminals. It provides both an interactive CLI
|
|
51
|
+
and a Rich-compatible `MapView` renderable for embedding maps in terminal
|
|
52
|
+
applications.
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
python3 -m pip install mapscii-py
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The distribution name is `mapscii-py`; the Python import package is
|
|
61
|
+
`mapscii_py`.
|
|
62
|
+
|
|
63
|
+
## Command line
|
|
64
|
+
|
|
65
|
+
Start the interactive map:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
mapscii
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Arrow keys or `hjkl` move the map, `a` zooms in, `z` zooms out, and `q` exits.
|
|
72
|
+
Render one non-interactive frame with:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
mapscii --once --width 80 --height 24
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The module entry point is equivalent:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
python3 -m mapscii_py --once
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Rich integration
|
|
85
|
+
|
|
86
|
+
`MapView` adapts its width to the surrounding Rich layout and keeps a fixed
|
|
87
|
+
number of terminal rows:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from rich.console import Console
|
|
91
|
+
|
|
92
|
+
from mapscii_py import MapView
|
|
93
|
+
|
|
94
|
+
map_view = MapView(
|
|
95
|
+
latitude=47.4979,
|
|
96
|
+
longitude=19.0402,
|
|
97
|
+
zoom=8,
|
|
98
|
+
height=16,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
Console().print(
|
|
102
|
+
map_view.panel(title="Budapest airspace")
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Markers can be overlaid without modifying the underlying map tiles:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from mapscii_py import MapMarker
|
|
110
|
+
|
|
111
|
+
map_view.set_markers([
|
|
112
|
+
MapMarker(
|
|
113
|
+
latitude=47.4979,
|
|
114
|
+
longitude=19.0402,
|
|
115
|
+
label="Budapest",
|
|
116
|
+
symbol="●",
|
|
117
|
+
),
|
|
118
|
+
])
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Development and publishing
|
|
122
|
+
|
|
123
|
+
Build and validate both the source distribution and wheel:
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
python3 -m pip install build twine
|
|
127
|
+
python3 -m build
|
|
128
|
+
python3 -m twine check dist/*
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Upload a release to PyPI:
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
python3 -m twine upload dist/*
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Increase the version in `pyproject.toml` and `mapscii_py/__init__.py` before
|
|
138
|
+
building a new release. PyPI does not allow an existing release file or
|
|
139
|
+
version to be replaced.
|
|
140
|
+
|
|
141
|
+
## Attribution and license
|
|
142
|
+
|
|
143
|
+
This implementation is derived from MapSCII, originally created by Michael
|
|
144
|
+
Straßburger and the MapSCII contributors. Map data is © OpenStreetMap
|
|
145
|
+
contributors.
|
|
146
|
+
|
|
147
|
+
The project is distributed under the MIT License; see `LICENSE`.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# mapscii-py
|
|
2
|
+
|
|
3
|
+
A Python port of [MapSCII](https://github.com/rastapasta/mapscii) for rendering
|
|
4
|
+
OpenStreetMap vector tiles in terminals. It provides both an interactive CLI
|
|
5
|
+
and a Rich-compatible `MapView` renderable for embedding maps in terminal
|
|
6
|
+
applications.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
python3 -m pip install mapscii-py
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The distribution name is `mapscii-py`; the Python import package is
|
|
15
|
+
`mapscii_py`.
|
|
16
|
+
|
|
17
|
+
## Command line
|
|
18
|
+
|
|
19
|
+
Start the interactive map:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
mapscii
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Arrow keys or `hjkl` move the map, `a` zooms in, `z` zooms out, and `q` exits.
|
|
26
|
+
Render one non-interactive frame with:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
mapscii --once --width 80 --height 24
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The module entry point is equivalent:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
python3 -m mapscii_py --once
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Rich integration
|
|
39
|
+
|
|
40
|
+
`MapView` adapts its width to the surrounding Rich layout and keeps a fixed
|
|
41
|
+
number of terminal rows:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from rich.console import Console
|
|
45
|
+
|
|
46
|
+
from mapscii_py import MapView
|
|
47
|
+
|
|
48
|
+
map_view = MapView(
|
|
49
|
+
latitude=47.4979,
|
|
50
|
+
longitude=19.0402,
|
|
51
|
+
zoom=8,
|
|
52
|
+
height=16,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
Console().print(
|
|
56
|
+
map_view.panel(title="Budapest airspace")
|
|
57
|
+
)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Markers can be overlaid without modifying the underlying map tiles:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from mapscii_py import MapMarker
|
|
64
|
+
|
|
65
|
+
map_view.set_markers([
|
|
66
|
+
MapMarker(
|
|
67
|
+
latitude=47.4979,
|
|
68
|
+
longitude=19.0402,
|
|
69
|
+
label="Budapest",
|
|
70
|
+
symbol="●",
|
|
71
|
+
),
|
|
72
|
+
])
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Development and publishing
|
|
76
|
+
|
|
77
|
+
Build and validate both the source distribution and wheel:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
python3 -m pip install build twine
|
|
81
|
+
python3 -m build
|
|
82
|
+
python3 -m twine check dist/*
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Upload a release to PyPI:
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
python3 -m twine upload dist/*
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Increase the version in `pyproject.toml` and `mapscii_py/__init__.py` before
|
|
92
|
+
building a new release. PyPI does not allow an existing release file or
|
|
93
|
+
version to be replaced.
|
|
94
|
+
|
|
95
|
+
## Attribution and license
|
|
96
|
+
|
|
97
|
+
This implementation is derived from MapSCII, originally created by Michael
|
|
98
|
+
Straßburger and the MapSCII contributors. Map data is © OpenStreetMap
|
|
99
|
+
contributors.
|
|
100
|
+
|
|
101
|
+
The project is distributed under the MIT License; see `LICENSE`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""A small Python port of MapSCII's vector-tile renderer."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from .renderer import Renderer
|
|
6
|
+
from .rich_map import MapMarker, MapView
|
|
7
|
+
from .tiles import HttpTileSource
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"HttpTileSource",
|
|
11
|
+
"MapMarker",
|
|
12
|
+
"MapView",
|
|
13
|
+
"Renderer",
|
|
14
|
+
"__version__",
|
|
15
|
+
]
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from typing import Iterable, List, Optional, Sequence, Tuple
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
BRAILLE_BITS = (
|
|
6
|
+
(0x01, 0x08),
|
|
7
|
+
(0x02, 0x10),
|
|
8
|
+
(0x04, 0x20),
|
|
9
|
+
(0x40, 0x80),
|
|
10
|
+
)
|
|
11
|
+
RESET = "\x1b[39;49m"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BrailleBuffer:
|
|
15
|
+
def __init__(self, width: int, height: int, use_braille: bool = True) -> None:
|
|
16
|
+
self.width = max(2, width - width % 2)
|
|
17
|
+
self.height = max(4, height - height % 4)
|
|
18
|
+
self.columns = self.width // 2
|
|
19
|
+
self.rows = self.height // 4
|
|
20
|
+
self.use_braille = use_braille
|
|
21
|
+
self.global_background: Optional[int] = None
|
|
22
|
+
self.clear()
|
|
23
|
+
|
|
24
|
+
def clear(self) -> None:
|
|
25
|
+
size = self.columns * self.rows
|
|
26
|
+
self.pixels = bytearray(size)
|
|
27
|
+
self.characters: List[Optional[str]] = [None] * size
|
|
28
|
+
self.foreground: List[Optional[int]] = [None] * size
|
|
29
|
+
self.background: List[Optional[int]] = [None] * size
|
|
30
|
+
|
|
31
|
+
def _index(self, x: int, y: int) -> int:
|
|
32
|
+
return x // 2 + self.columns * (y // 4)
|
|
33
|
+
|
|
34
|
+
def set_pixel(self, x: int, y: int, color: Optional[int]) -> None:
|
|
35
|
+
if not (0 <= x < self.width and 0 <= y < self.height):
|
|
36
|
+
return
|
|
37
|
+
index = self._index(x, y)
|
|
38
|
+
self.pixels[index] |= BRAILLE_BITS[y & 3][x & 1]
|
|
39
|
+
if color is not None:
|
|
40
|
+
self.foreground[index] = color
|
|
41
|
+
|
|
42
|
+
def set_character(self, character: str, x: int, y: int, color: Optional[int]) -> None:
|
|
43
|
+
if not (0 <= x < self.width and 0 <= y < self.height):
|
|
44
|
+
return
|
|
45
|
+
index = self._index(x, y)
|
|
46
|
+
self.characters[index] = character
|
|
47
|
+
if color is not None:
|
|
48
|
+
self.foreground[index] = color
|
|
49
|
+
|
|
50
|
+
def write_text(self, text: str, x: int, y: int, color: Optional[int], center: bool = False) -> None:
|
|
51
|
+
if center:
|
|
52
|
+
x -= len(text)
|
|
53
|
+
for offset, character in enumerate(text):
|
|
54
|
+
self.set_character(character, x + offset * 2, y, color)
|
|
55
|
+
|
|
56
|
+
def set_background(self, color: Optional[int]) -> None:
|
|
57
|
+
self.global_background = color
|
|
58
|
+
|
|
59
|
+
def _color_sequence(self, foreground: Optional[int], background: Optional[int]) -> str:
|
|
60
|
+
background = background if background is not None else self.global_background
|
|
61
|
+
if foreground is not None and background is not None:
|
|
62
|
+
return "\x1b[38;5;{};48;5;{}m".format(foreground, background)
|
|
63
|
+
if foreground is not None:
|
|
64
|
+
return "\x1b[49;38;5;{}m".format(foreground)
|
|
65
|
+
if background is not None:
|
|
66
|
+
return "\x1b[39;48;5;{}m".format(background)
|
|
67
|
+
return RESET
|
|
68
|
+
|
|
69
|
+
def frame(self) -> str:
|
|
70
|
+
output: List[str] = []
|
|
71
|
+
current_color: Optional[str] = None
|
|
72
|
+
for row in range(self.rows):
|
|
73
|
+
skip = 0
|
|
74
|
+
for column in range(self.columns):
|
|
75
|
+
index = row * self.columns + column
|
|
76
|
+
color = self._color_sequence(self.foreground[index], self.background[index])
|
|
77
|
+
if color != current_color:
|
|
78
|
+
output.append(color)
|
|
79
|
+
current_color = color
|
|
80
|
+
|
|
81
|
+
character = self.characters[index]
|
|
82
|
+
if character is not None:
|
|
83
|
+
output.append(character)
|
|
84
|
+
skip += max(0, len(character) - 1)
|
|
85
|
+
elif skip:
|
|
86
|
+
skip -= 1
|
|
87
|
+
elif self.use_braille:
|
|
88
|
+
output.append(chr(0x2800 + self.pixels[index]))
|
|
89
|
+
else:
|
|
90
|
+
output.append(self._block_character(self.pixels[index]))
|
|
91
|
+
output.append(RESET + "\n")
|
|
92
|
+
return "".join(output)
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def _block_character(mask: int) -> str:
|
|
96
|
+
if mask == 0:
|
|
97
|
+
return " "
|
|
98
|
+
upper = mask & (0x01 | 0x02 | 0x08 | 0x10)
|
|
99
|
+
lower = mask & (0x04 | 0x40 | 0x20 | 0x80)
|
|
100
|
+
if upper and lower:
|
|
101
|
+
return "█"
|
|
102
|
+
if upper:
|
|
103
|
+
return "▀"
|
|
104
|
+
return "▄"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Canvas:
|
|
108
|
+
def __init__(self, width: int, height: int, use_braille: bool = True) -> None:
|
|
109
|
+
self.width = width
|
|
110
|
+
self.height = height
|
|
111
|
+
self.buffer = BrailleBuffer(width, height, use_braille)
|
|
112
|
+
|
|
113
|
+
def clear(self) -> None:
|
|
114
|
+
self.buffer.clear()
|
|
115
|
+
|
|
116
|
+
def frame(self) -> str:
|
|
117
|
+
return self.buffer.frame()
|
|
118
|
+
|
|
119
|
+
def set_background(self, color: Optional[int]) -> None:
|
|
120
|
+
self.buffer.set_background(color)
|
|
121
|
+
|
|
122
|
+
def text(self, text: str, x: int, y: int, color: Optional[int], center: bool = False) -> None:
|
|
123
|
+
self.buffer.write_text(text, x, y, color, center)
|
|
124
|
+
|
|
125
|
+
def polyline(self, points: Sequence[Tuple[int, int]], color: Optional[int], width: int = 1) -> None:
|
|
126
|
+
for start, end in zip(points, points[1:]):
|
|
127
|
+
self.line(start, end, color, width)
|
|
128
|
+
|
|
129
|
+
def line(self, start: Tuple[int, int], end: Tuple[int, int], color: Optional[int], width: int = 1) -> None:
|
|
130
|
+
x0, y0 = start
|
|
131
|
+
x1, y1 = end
|
|
132
|
+
radius = max(0, int(round(width)) - 1) // 2
|
|
133
|
+
for x, y in self._bresenham(x0, y0, x1, y1):
|
|
134
|
+
for dy in range(-radius, radius + 1):
|
|
135
|
+
for dx in range(-radius, radius + 1):
|
|
136
|
+
self.buffer.set_pixel(x + dx, y + dy, color)
|
|
137
|
+
|
|
138
|
+
def polygon(self, rings: Sequence[Sequence[Tuple[int, int]]], color: Optional[int]) -> None:
|
|
139
|
+
edges = []
|
|
140
|
+
for ring in rings:
|
|
141
|
+
if len(ring) < 3:
|
|
142
|
+
continue
|
|
143
|
+
closed = list(ring)
|
|
144
|
+
if closed[0] != closed[-1]:
|
|
145
|
+
closed.append(closed[0])
|
|
146
|
+
edges.extend(zip(closed, closed[1:]))
|
|
147
|
+
|
|
148
|
+
for y in range(self.height):
|
|
149
|
+
intersections: List[float] = []
|
|
150
|
+
scan_y = y + 0.5
|
|
151
|
+
for (x1, y1), (x2, y2) in edges:
|
|
152
|
+
if y1 == y2:
|
|
153
|
+
continue
|
|
154
|
+
if min(y1, y2) <= scan_y < max(y1, y2):
|
|
155
|
+
intersections.append(x1 + (scan_y - y1) * (x2 - x1) / (y2 - y1))
|
|
156
|
+
intersections.sort()
|
|
157
|
+
for left, right in zip(intersections[0::2], intersections[1::2]):
|
|
158
|
+
for x in range(max(0, math.ceil(left)), min(self.width - 1, math.floor(right)) + 1):
|
|
159
|
+
self.buffer.set_pixel(x, y, color)
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def _bresenham(x0: int, y0: int, x1: int, y1: int) -> Iterable[Tuple[int, int]]:
|
|
163
|
+
dx = abs(x1 - x0)
|
|
164
|
+
sx = 1 if x0 < x1 else -1
|
|
165
|
+
dy = -abs(y1 - y0)
|
|
166
|
+
sy = 1 if y0 < y1 else -1
|
|
167
|
+
error = dx + dy
|
|
168
|
+
while True:
|
|
169
|
+
yield x0, y0
|
|
170
|
+
if x0 == x1 and y0 == y1:
|
|
171
|
+
return
|
|
172
|
+
twice_error = 2 * error
|
|
173
|
+
if twice_error >= dy:
|
|
174
|
+
error += dy
|
|
175
|
+
x0 += sx
|
|
176
|
+
if twice_error <= dx:
|
|
177
|
+
error += dx
|
|
178
|
+
y0 += sy
|
|
179
|
+
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import math
|
|
3
|
+
import os
|
|
4
|
+
import select
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
import termios
|
|
8
|
+
import tty
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional, Sequence, Tuple
|
|
11
|
+
|
|
12
|
+
from .renderer import Renderer
|
|
13
|
+
from .style import Styler
|
|
14
|
+
from .tiles import HttpTileSource
|
|
15
|
+
from .utils import base_zoom, lonlat_to_tile, normalize, tile_size_at_zoom, tile_to_lonlat
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DEFAULT_SOURCE = "http://mapscii.me/"
|
|
19
|
+
DEFAULT_LATITUDE = 52.51298
|
|
20
|
+
DEFAULT_LONGITUDE = 13.42012
|
|
21
|
+
DEFAULT_STYLE = Path(__file__).resolve().parent / "styles" / "dark.json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_arguments(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
|
|
25
|
+
parser = argparse.ArgumentParser(description="MapSCII's vector-tile renderer ported to Python")
|
|
26
|
+
parser.add_argument("--latitude", "--lat", type=float, default=DEFAULT_LATITUDE)
|
|
27
|
+
parser.add_argument("--longitude", "--lon", type=float, default=DEFAULT_LONGITUDE)
|
|
28
|
+
parser.add_argument("--zoom", "-z", type=float)
|
|
29
|
+
parser.add_argument("--width", "-w", type=int, help="map width in terminal columns")
|
|
30
|
+
parser.add_argument("--height", type=int, help="map height in terminal rows")
|
|
31
|
+
parser.add_argument("--source", default=DEFAULT_SOURCE)
|
|
32
|
+
parser.add_argument("--style", type=Path, default=DEFAULT_STYLE)
|
|
33
|
+
parser.add_argument("--ascii", action="store_true", help="use block characters instead of Braille")
|
|
34
|
+
parser.add_argument("--no-cache", action="store_true")
|
|
35
|
+
parser.add_argument("--cache-directory", type=Path)
|
|
36
|
+
parser.add_argument("--once", action="store_true", help="render one frame and exit")
|
|
37
|
+
return parser.parse_args(argv)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Application:
|
|
41
|
+
def __init__(self, arguments: argparse.Namespace) -> None:
|
|
42
|
+
columns, rows = shutil.get_terminal_size((80, 24))
|
|
43
|
+
self.columns = arguments.width or columns
|
|
44
|
+
self.rows = arguments.height or max(2, rows - 1)
|
|
45
|
+
self.latitude = arguments.latitude
|
|
46
|
+
self.longitude = arguments.longitude
|
|
47
|
+
pixel_width = self.columns * 2
|
|
48
|
+
pixel_height = self.rows * 4
|
|
49
|
+
self.minimum_zoom = 4 - math.log2(4096 / pixel_width)
|
|
50
|
+
self.zoom = arguments.zoom if arguments.zoom is not None else self.minimum_zoom
|
|
51
|
+
self.zoom = max(self.minimum_zoom, min(18.0, self.zoom))
|
|
52
|
+
|
|
53
|
+
styler = Styler.from_file(arguments.style)
|
|
54
|
+
source = HttpTileSource(
|
|
55
|
+
arguments.source,
|
|
56
|
+
styler,
|
|
57
|
+
cache_directory=arguments.cache_directory,
|
|
58
|
+
persist=not arguments.no_cache,
|
|
59
|
+
)
|
|
60
|
+
self.renderer = Renderer(source, styler, pixel_width, pixel_height, not arguments.ascii)
|
|
61
|
+
|
|
62
|
+
def frame(self) -> str:
|
|
63
|
+
return self.renderer.draw(self.latitude, self.longitude, self.zoom)
|
|
64
|
+
|
|
65
|
+
def draw(self, clear: bool = True) -> None:
|
|
66
|
+
prefix = "\x1b[2J\x1b[H" if clear else ""
|
|
67
|
+
footer = "center: {:.3f}, {:.3f} zoom: {:.2f} arrows/hjkl move a/z zoom q quit © OSM\n".format(
|
|
68
|
+
self.latitude, self.longitude, self.zoom
|
|
69
|
+
)
|
|
70
|
+
sys.stdout.write(prefix + self.frame() + "\x1b[0m" + footer)
|
|
71
|
+
sys.stdout.flush()
|
|
72
|
+
|
|
73
|
+
def move(self, dx: float, dy: float) -> None:
|
|
74
|
+
z = base_zoom(self.zoom)
|
|
75
|
+
center_x, center_y = lonlat_to_tile(self.longitude, self.latitude, z)
|
|
76
|
+
size = tile_size_at_zoom(self.zoom)
|
|
77
|
+
lon, lat = tile_to_lonlat(center_x + dx / size, center_y + dy / size, z)
|
|
78
|
+
self.longitude, self.latitude = normalize(lon, lat)
|
|
79
|
+
|
|
80
|
+
def run(self) -> None:
|
|
81
|
+
old_settings = termios.tcgetattr(sys.stdin.fileno())
|
|
82
|
+
try:
|
|
83
|
+
tty.setcbreak(sys.stdin.fileno())
|
|
84
|
+
self.draw()
|
|
85
|
+
while True:
|
|
86
|
+
key = os.read(sys.stdin.fileno(), 1)
|
|
87
|
+
if key == b"\x1b":
|
|
88
|
+
while select.select([sys.stdin], [], [], 0.01)[0]:
|
|
89
|
+
key += os.read(sys.stdin.fileno(), 1)
|
|
90
|
+
if key in (b"q", b"Q"):
|
|
91
|
+
return
|
|
92
|
+
if key in (b"a",):
|
|
93
|
+
self.zoom = min(18.0, self.zoom + 0.2)
|
|
94
|
+
elif key in (b"z", b"y"):
|
|
95
|
+
self.zoom = max(self.minimum_zoom, self.zoom - 0.2)
|
|
96
|
+
elif key in (b"h", b"\x1b[D"):
|
|
97
|
+
self.move(-16, 0)
|
|
98
|
+
elif key in (b"l", b"\x1b[C"):
|
|
99
|
+
self.move(16, 0)
|
|
100
|
+
elif key in (b"k", b"\x1b[A"):
|
|
101
|
+
self.move(0, -12)
|
|
102
|
+
elif key in (b"j", b"\x1b[B"):
|
|
103
|
+
self.move(0, 12)
|
|
104
|
+
else:
|
|
105
|
+
continue
|
|
106
|
+
self.draw()
|
|
107
|
+
finally:
|
|
108
|
+
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, old_settings)
|
|
109
|
+
sys.stdout.write("\x1b[0m\n")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main(argv: Optional[Sequence[str]] = None) -> None:
|
|
113
|
+
arguments = parse_arguments(argv)
|
|
114
|
+
application = Application(arguments)
|
|
115
|
+
if arguments.once:
|
|
116
|
+
application.draw(clear=False)
|
|
117
|
+
else:
|
|
118
|
+
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
119
|
+
raise SystemExit("interactive mode requires a TTY; use --once for redirected output")
|
|
120
|
+
application.run()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|