├── .gitignore ├── .travis.yml ├── CHANGES.md ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── examples ├── cli-tutorial.sh ├── colors.py ├── complex-render.py ├── paginate.py └── simple-render.py ├── gj2ascii ├── __init__.py ├── cli.py ├── core.py └── pycompat.py ├── images ├── ascii-land-cover-80w.png └── emoji-land-cover-80w.png ├── sample-data ├── WV.geojson ├── bbox.geojson ├── lines.geojson ├── multilayer-polygon-line │ ├── lines.dbf │ ├── lines.prj │ ├── lines.shp │ ├── lines.shx │ ├── polygons.dbf │ ├── polygons.prj │ ├── polygons.shp │ └── polygons.shx ├── points.geojson ├── polygons.geojson ├── single-feature-WV.geojson └── small-aoi-polygon-line.geojson ├── setup.cfg ├── setup.py └── tests ├── conftest.py ├── test_cli.py └── test_core.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | venv/ 12 | venv2/ 13 | venv3/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *,cover 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | 59 | # Sphinx documentation 60 | docs/_build/ 61 | 62 | # PyBuilder 63 | target/ 64 | 65 | # Intellij PyCharm 66 | .idea/ 67 | 68 | # Project 69 | ignore/ 70 | buh.py 71 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | sudo: false 4 | 5 | cache: 6 | directories: 7 | - ~/.cache/pip 8 | 9 | env: 10 | global: 11 | - PIP_WHEEL_DIR=$HOME/.cache/pip/wheels 12 | - PIP_FIND_LINKS=file://$HOME/.cache/pip/wheels 13 | 14 | addons: 15 | apt: 16 | packages: 17 | - gdal-bin 18 | - gfortran 19 | - libatlas-base-dev 20 | - libatlas-dev 21 | - libgdal-dev 22 | - libgdal1h 23 | - libgeos-dev 24 | - libhdf5-serial-dev 25 | - libpng12-dev 26 | - libproj-dev 27 | 28 | python: 29 | - 2.7 30 | - 3.6 31 | 32 | before_install: 33 | - pip install pip wheel setuptools coveralls --upgrade 34 | 35 | install: 36 | - pip install .\[all\] 37 | 38 | script: 39 | - pytest tests --cov gj2ascii --cov-report term-missing 40 | 41 | after_success: 42 | - coveralls 43 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | Changes 2 | ======= 3 | 4 | Version 0.4.1 (2015-06-02) 5 | -------------------------- 6 | 7 | Fixed formatting issues in README.rst 8 | 9 | 10 | Version 0.4 (2015-06-02) 11 | ------------------------ 12 | 13 | Width arguments now specify number of text columns rather than raster width - #30 14 | New `min_bbox()` function to easily compute the minimum bbox from geometry iterators - #27 15 | New `--colors` flag that prints a list of available colors and exits - #37 16 | New `style_multiple()` function to easily render and apply colors to multiple layers in the API - #20 17 | New `render_multiple()` function to easily render multiple layers in the API - #20 18 | New `ascii2array()` and `array2ascii()` functions 19 | Support for colors and multiple layers on the commandline and in the API - #14, #15 20 | Zoom in on a specific area with bbox flag and param for `render()` - #14 21 | New `stack()` function to overlay rendered layers - #14 22 | New `paginate()` function to easily iterate over features for the CLI - #13 23 | New `style()` function to apply colors to a rendering - #15 24 | `render()` now has a default width - #12 25 | `render()` is now smarter about handling GeoJSON geometry, features, or other iterables - #11 26 | Removed `--no-all-touched`, `-at`, `-nat`, and `-i` in favor of just `--all-touched` and `--iterate` - #29 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | New BSD License 2 | 3 | Copyright (c) 2015, Kevin D. Wurster 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * The names of its contributors may not be used to endorse or promote products 17 | derived from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGES.md 2 | include LICENSE.txt 3 | include MANIFEST.in 4 | include README.rst 5 | include setup.py 6 | include setup.cfg 7 | recursive-include tests *.py 8 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. code-block:: console 2 | 3 | _ ___ _ _ 4 | ____ _ (_)__ \ ____ ___________(_|_) 5 | / __ `/ / /__/ // __ `/ ___/ ___/ / / 6 | / /_/ / / // __// /_/ (__ ) /__/ / / 7 | \__, /_/ //____/\__,_/____/\___/_/_/ 8 | /____/___/ 9 | 10 | 11 | .. image:: https://travis-ci.org/geowurster/gj2ascii.svg?branch=master 12 | :target: https://travis-ci.org/geowurster/gj2ascii 13 | 14 | 15 | .. image:: https://coveralls.io/repos/geowurster/gj2ascii/badge.svg?branch=master 16 | :target: https://coveralls.io/r/geowurster/gj2ascii 17 | 18 | Render spatial vector data as ASCII or emoji with Python on the commandline. 19 | 20 | .. image:: https://raw.githubusercontent.com/geowurster/gj2ascii/master/images/emoji-land-cover-80w.png 21 | :target: https://github.com/geowurster/gj2ascii/blob/master/images/emoji-land-cover-80w.png 22 | :width: 574 23 | :height: 563 24 | 25 | 26 | Why? 27 | ==== 28 | 29 | A `tweet `__ made it seem like an interesting exercise but 30 | the ``gj2ascii`` commandline utility has been very useful for previewing multiple files and the API has proven to be 31 | useful for debugging complex geoprocessing operations. 32 | 33 | 34 | Default Behavior 35 | ================ 36 | 37 | The overall goal of this utility is to provide easy access to ASCII representations 38 | of vector geometries and overlapping layers. 39 | 40 | 1. When rendering a single layer an ASCII character is used. 41 | 2. When rendering multiple layers colors are used with a randomly assigned character underneath and a transparent fill. 42 | 43 | There are only ``7`` colors (black is not used for auto-assignment) so if more than 7 layers 44 | are given the ``--char`` flag must be used for each of them to manually assign a character, 45 | emoji, or color. 46 | 47 | 48 | Emoji Example 49 | ============= 50 | 51 | The emoji screenshot was generated by downloading the ``GeoTIFF`` version of the 52 | `MODIS Landcover dataset `_, resampling to ``25%`` 53 | of its original, size converted to a vector with `gdal_polygonize.py `_, 54 | and `split `_ 55 | into one ``ESRI Shapefile`` per class with `QGIS `_, before executing the following 56 | command: 57 | 58 | .. code-block:: console 59 | 60 | $ gj2ascii \ 61 | --bbox -130 9 -61.5 77 \ 62 | --width 80 \ 63 | 0.geojson -c ' ' \ 64 | 1.geojson -c :christmas_tree: \ 65 | 2.geojson -c :evergreen_tree: \ 66 | 3.geojson -c :maple_leaf: \ 67 | 4.geojson -c :maple_leaf: \ 68 | 5.geojson -c :deciduous_tree: \ 69 | 6.geojson -c :herb: \ 70 | 7.geojson -c :herb: \ 71 | 8.geojson -c :herb: \ 72 | 9.geojson -c :herb: \ 73 | 10.geojson -c :ear_of_rice: \ 74 | 11.geojson -c :turtle: \ 75 | 12.geojson -c :tractor: \ 76 | 13.geojson -c :house_building: \ 77 | 14.geojson -c :leaf_fluttering_in_wind: \ 78 | 15.geojson -c :snowflake: \ 79 | 16.geojson -c :black_medium_square: 80 | 81 | The same data can be rendered with ASCII characters instead: 82 | 83 | .. code-block:: console 84 | 85 | $ gj2ascii \ 86 | --bbox -130 9 -61.5 77 \ 87 | --width 80 \ 88 | 0.shp -c ' ' \ 89 | 1.shp -c \# \ 90 | 2.shp -c \^ \ 91 | 3.shp -c + \ 92 | 4.shp -c \& \ 93 | 5.shp -c \$ \ 94 | 6.shp -c \% \ 95 | 7.shp -c \: \ 96 | 8.shp -c P \ 97 | 9.shp -c - \ 98 | 10.shp -c \" \ 99 | 11.shp -c 0 \ 100 | 12.shp -c = \ 101 | 13.shp -c N \ 102 | 14.shp -c \@ \ 103 | 15.shp -c \* \ 104 | 16.shp -c O 105 | 106 | 107 | .. image:: https://raw.githubusercontent.com/geowurster/gj2ascii/master/images/ascii-land-cover-80w.png 108 | :target: https://github.com/geowurster/gj2ascii/tree/master/images/ascii-land-cover-80w.png 109 | :width: 566 110 | :height: 553 111 | 112 | 113 | Other Examples 114 | ============== 115 | 116 | See the `examples directory `__ for more information and 117 | more complex examples but the following are a good place to get started. Some of the examples include output that 118 | would be colored if run on the commandline or in Python but RST cannot render the ANSI codes. 119 | 120 | Render two layers, one read from stin and one read directly from a file, across 20 pixels while explicitly specifying 121 | a character and color for each layer and background fill, and zooming in on an area of interest. 122 | 123 | .. code-block:: console 124 | 125 | $ cat sample-data/polygons.geojson | gj2ascii - \ 126 | sample-data/lines.geojson \ 127 | --bbox sample-data/small-aoi-polygon-line.geojson \ 128 | --width 20 \ 129 | --char ^=red \ 130 | --char -=blue \ 131 | --fill .=green 132 | . . . . . . - . . . . . . . . . ^ ^ ^ ^ 133 | . . . . . - . . . . . . . . . . . ^ ^ ^ 134 | . . . . - . . . . . . . . . . . . . - - 135 | . . . . - . . . . . . . . - - - - - . ^ 136 | ^ ^ . - . . . . . . . . . . . . . . . . 137 | ^ ^ - . . . . . . . . . . . . . . . . . 138 | ^ - ^ . . . . . . . . . . . . . . . . . 139 | ^ - . . . . . . . . . . . . . . . . . . 140 | - ^ . . . . . . - . . . . . ^ . . . . . 141 | . - . . . . . . - - . . . ^ ^ . . . . . 142 | . . - . . . . . - . - . ^ ^ ^ . . . . . 143 | . . . - . . . . - . . - ^ ^ ^ . . . . . 144 | . . . . - . . - . . ^ ^ - ^ ^ . . . . . 145 | . . . . . - . - . ^ ^ ^ ^ - ^ . . . . . 146 | . . . . . . - - ^ ^ ^ ^ ^ ^ - . . . . . 147 | 148 | 149 | 150 | Render individual features across 10 pixels and display the attributes for two 151 | fields, ``COUNTYFP`` and ``NAME``. 152 | 153 | .. code-block:: console 154 | 155 | $ gj2ascii sample-data/WV.geojson \ 156 | --iterate \ 157 | --properties COUNTYFP,NAME \ 158 | --width 10 159 | 160 | +----------+---------+ 161 | | COUNTYFP | 001 | 162 | | NAME | Barbour | 163 | +----------+---------+ 164 | 165 | + + + 166 | + + + + + + + + 167 | + + + + + + + + + 168 | + + + + + + + + + 169 | + + + + + + + + + + 170 | + + + + + + + 171 | + + + + 172 | + + + + 173 | 174 | Press enter for the next geometry or ^C/^D or 'q' to quit... 175 | 176 | Recreate the first example with the Python API 177 | ---------------------------------------------- 178 | 179 | There are two ways to recreate the first example with the Python API. If the user does not care about which characters 180 | are assigned to which color, use this one: 181 | 182 | .. code-block:: python 183 | 184 | import fiona as fio 185 | import gj2ascii 186 | with fio.open('sample-data/polygons.geojson') as poly, \ 187 | fio.open('sample-data/lines.geojson') as lines, \ 188 | fio.open('sample-data/small-aoi-polygon-line.geojson') as bbox: 189 | layermap = [ 190 | (poly, 'red'), 191 | (lines, 'blue') 192 | ] 193 | print(gj2ascii.style_multiple(layermap, 20, fill='green', bbox=bbox.bounds)) 194 | 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 2 2 2 2 195 | 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 2 2 2 196 | 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 197 | 0 0 0 0 1 0 0 0 0 0 0 0 0 1 1 1 1 1 0 2 198 | 2 2 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 199 | 2 2 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 200 | 2 1 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 201 | 2 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 202 | 1 2 0 0 0 0 0 0 1 0 0 0 0 0 2 0 0 0 0 0 203 | 0 1 0 0 0 0 0 0 1 1 0 0 0 2 2 0 0 0 0 0 204 | 0 0 1 0 0 0 0 0 1 0 1 0 2 2 2 0 0 0 0 0 205 | 0 0 0 1 0 0 0 0 1 0 0 1 2 2 2 0 0 0 0 0 206 | 0 0 0 0 1 0 0 1 0 0 2 2 1 2 2 0 0 0 0 0 207 | 0 0 0 0 0 1 0 1 0 2 2 2 2 1 2 0 0 0 0 0 208 | 0 0 0 0 0 0 1 1 2 2 2 2 2 2 1 0 0 0 0 0 209 | 210 | 211 | If the user cares about which character is assigned to which layer, use this one: 212 | 213 | .. code-block:: python 214 | 215 | import fiona as fio 216 | import gj2ascii 217 | 218 | with fio.open('sample-data/polygons.geojson') as poly, \ 219 | fio.open('sample-data/lines.geojson') as lines, \ 220 | fio.open('sample-data/small-aoi-polygon-line.geojson') as bbox: 221 | 222 | # Render each layer individually with the same bbox and width 223 | # The fill will be assigned in the next step but must be a single space here 224 | rendered_layers = [ 225 | gj2ascii.render(poly, 20, char='^', fill=' ', bbox=bbox.bounds), 226 | gj2ascii.render(lines, 20, char='-', fill=' ', bbox=bbox.bounds) 227 | ] 228 | 229 | # Overlay the rendered layers into one stack 230 | stacked = gj2ascii.stack(rendered_layers, fill='.') 231 | 232 | # Apply the colors and print 233 | colormap = { 234 | '^': 'red', 235 | '-': 'blue', 236 | '.': 'green' 237 | } 238 | print(gj2ascii.style(stacked, colormap)) 239 | . . . . . . - . . . . . . . . . ^ ^ ^ ^ 240 | . . . . . - . . . . . . . . . . . ^ ^ ^ 241 | . . . . - . . . . . . . . . . . . . - - 242 | . . . . - . . . . . . . . - - - - - . ^ 243 | ^ ^ . - . . . . . . . . . . . . . . . . 244 | ^ ^ - . . . . . . . . . . . . . . . . . 245 | ^ - ^ . . . . . . . . . . . . . . . . . 246 | ^ - . . . . . . . . . . . . . . . . . . 247 | - ^ . . . . . . - . . . . . ^ . . . . . 248 | . - . . . . . . - - . . . ^ ^ . . . . . 249 | . . - . . . . . - . - . ^ ^ ^ . . . . . 250 | . . . - . . . . - . . - ^ ^ ^ . . . . . 251 | . . . . - . . - . . ^ ^ - ^ ^ . . . . . 252 | . . . . . - . - . ^ ^ ^ ^ - ^ . . . . . 253 | . . . . . . - - ^ ^ ^ ^ ^ ^ - . . . . . 254 | 255 | Paginating through features: 256 | 257 | .. code-block:: python 258 | 259 | import fiona as fio 260 | import gj2ascii 261 | 262 | with fio.open('sample-data/WV.geojson') as src: 263 | for feature in gj2ascii.paginate(src, 10, properties=['COUNTYFP', 'NAME']): 264 | print(feature) 265 | +----------+---------+ 266 | | COUNTYFP | 001 | 267 | | NAME | Barbour | 268 | +----------+---------+ 269 | 270 | + + + 271 | + + + + + + + + 272 | + + + + + + + + + 273 | + + + + + + + + + 274 | + + + + + + + + + + 275 | + + + + + + + 276 | + + + + 277 | + + + + 278 | 279 | 280 | Installation 281 | ============ 282 | 283 | Via pip: 284 | 285 | .. code-block:: console 286 | 287 | $ pip install gj2ascii --upgrade 288 | 289 | From master branch: 290 | 291 | .. code-block:: console 292 | 293 | $ git clone https://github.com/geowurster/gj2ascii.git 294 | $ cd gj2ascii 295 | $ python setup.py install 296 | 297 | To enable emoji: 298 | 299 | .. code-block:: console 300 | 301 | $ pip install gj2ascii[emoji] 302 | 303 | 304 | Dependencies 305 | ------------ 306 | 307 | The dependencies are pretty heavy for a utility like this and may require some 308 | extra work to get everything installed. All dependencies should install on their 309 | own but there are a few potentially problematic packages. Manually installing 310 | the following might help: 311 | 312 | * `Rasterio `__ 313 | * `Fiona `__ 314 | * `Shapely `__ 315 | 316 | Some Linux distributions require an additional step before installing rasterio: 317 | ``apt-get install python-numpy-dev libgdal1h libgdal-dev``. 318 | 319 | 320 | Developing 321 | ========== 322 | 323 | .. code-block:: console 324 | 325 | $ git clone https://github.com/geowurster/gj2ascii.git 326 | $ cd gj2ascii 327 | $ virtualenv venv 328 | $ source venv/bin/activate 329 | $ pip install -e .[all] 330 | $ py.test gj2ascii --cov gj2ascii --cov-report term-missing 331 | 332 | 333 | License 334 | ======= 335 | 336 | See ``LICENSE.txt``. 337 | -------------------------------------------------------------------------------- /examples/cli-tutorial.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ### Simple commands ### 4 | 5 | # Render GeoJSON piped from stdin 6 | cat sample-data/polygons.geojson | gj2ascii - 7 | 8 | # Render a file with a single layer with a user-specified char and fill 9 | # and a non-default width 10 | gj2ascii sample-data/WV.geojson -w 10 -c \* -f . 11 | 12 | # Iterate over every feature in a layer but don't print an attribute table 13 | gj2ascii sample-data/WV.geojson --iterate 14 | 15 | # Same as above but print two attributes 16 | gj2ascii sample-data/WV.geojson --iterate -p ALAND,AWATER 17 | 18 | # Same as above but print all attributes 19 | gj2ascii sample-data/WV.geojson --iterate -p %all 20 | 21 | # Render multiple single-layer files 22 | gj2ascii sample-data/polygons.geojson sample-data/lines.geojson 23 | 24 | 25 | ### Advanced Commands ### 26 | 27 | # Same as above but turn off the colors and use user-defined characters 28 | gj2ascii sample-data/polygons.geojson sample-data/lines.geojson -c : -c ^ 29 | 30 | # Same as above but assign a specific color to each character 31 | gj2ascii sample-data/polygons.geojson sample-data/lines.geojson \ 32 | -c :=yellow \ 33 | -c ^=blue \ 34 | -f @=black 35 | 36 | # Render a single multi-layer file 37 | gj2ascii sample-data/multilayer-polygon-line/ 38 | 39 | # Mix rendering single and multi-layer files 40 | gj2ascii sample-data/multilayer-polygon-line/,polygons sample-data/lines.geojson 41 | 42 | # Same as above but clip to the bounds of a different layer 43 | gj2ascii sample-data/multilayer-polygon-line/,polygons sample-data/lines.geojson \ 44 | --bbox sample-data/bbox.geojson 45 | -------------------------------------------------------------------------------- /examples/colors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | """ 5 | Render data with colors 6 | """ 7 | 8 | 9 | import fiona as fio 10 | import gj2ascii 11 | 12 | 13 | print("Render a single layer with colors") 14 | with fio.open('sample-data/polygons.geojson') as src: 15 | rendered = gj2ascii.render(src, 40, char='+') 16 | print(gj2ascii.style(rendered, stylemap={'+': 'red'})) 17 | 18 | print("") 19 | 20 | print("Render multiple overlapping layers , apply colors, and zoom in on a bbox") 21 | with fio.open('sample-data/polygons.geojson') as poly, \ 22 | fio.open('sample-data/lines.geojson') as lines, \ 23 | fio.open('sample-data/small-aoi-polygon-line.geojson') as bbox: 24 | layermap = [ 25 | (poly, 'red'), 26 | (lines, 'blue') 27 | ] 28 | print(gj2ascii.style_multiple(layermap, 40, fill='yellow', bbox=bbox.bounds)) 29 | -------------------------------------------------------------------------------- /examples/complex-render.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | """ 5 | Render multiple overlapping layers and zoom in on an area of interest 6 | """ 7 | 8 | 9 | import gj2ascii 10 | import fiona as fio 11 | 12 | 13 | print("Render both layers") 14 | with fio.open('sample-data/polygons.geojson') as poly, \ 15 | fio.open('sample-data/lines.geojson') as lines, \ 16 | fio.open('sample-data/small-aoi-polygon-line.geojson') as bbox: 17 | layermap = [ 18 | (poly[0], '0'), 19 | (lines[0], '1') 20 | ] 21 | print(gj2ascii.render_multiple(layermap, 40, fill='.', bbox=bbox.bounds)) 22 | 23 | print("") 24 | 25 | print("Render one feature from each layer") 26 | with fio.open('sample-data/polygons.geojson') as poly, \ 27 | fio.open('sample-data/lines.geojson') as lines, \ 28 | fio.open('sample-data/small-aoi-polygon-line.geojson') as bbox: 29 | layermap = [ 30 | (poly[0], '0'), 31 | (lines[0], '1') 32 | ] 33 | print(gj2ascii.render_multiple(layermap, 40, fill='.', bbox=bbox.bounds)) 34 | 35 | print("") 36 | 37 | print("Render one feature and one geometry that are both independent from a Fiona") 38 | print("collection. In this case the bbox will be computed on the fly") 39 | with fio.open('sample-data/polygons.geojson') as poly, \ 40 | fio.open('sample-data/lines.geojson') as lines: 41 | feature = poly[0] 42 | geometry = lines[0]['geometry'] 43 | 44 | layermap = [ 45 | (feature, '0'), 46 | (geometry, '1') 47 | ] 48 | print(gj2ascii.render_multiple(layermap, 40)) 49 | -------------------------------------------------------------------------------- /examples/paginate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | """ 5 | Page through multiple features and display an attribute table for each 6 | """ 7 | 8 | 9 | import fiona as fio 10 | import gj2ascii 11 | 12 | 13 | print("Render every feature and its attribute table") 14 | with fio.open('sample-data/WV.geojson') as src: 15 | for feature in gj2ascii.paginate(src, properties=list(src.schema['properties'].keys())): 16 | print(feature) 17 | -------------------------------------------------------------------------------- /examples/simple-render.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | """ 5 | Rendering an entire layer and a single user-defined GeoJSON geometry object 6 | with the most common parameters. 7 | """ 8 | 9 | 10 | import os 11 | 12 | import gj2ascii 13 | import fiona as fio 14 | 15 | 16 | diamond = { 17 | 'type': 'LineString', 18 | 'coordinates': [[-10, 0], [0, 10], [10, 0], [0, -10], [-10, 0]] 19 | } 20 | 21 | print("Render a single independent geometry") 22 | print(gj2ascii.render(diamond, 20, char='*', fill='.')) 23 | 24 | print("") 25 | 26 | print("Render an entire layer") 27 | with fio.open('sample-data/WV.geojson') as src: 28 | print(gj2ascii.render(src, width=20, char='*', fill='.')) 29 | -------------------------------------------------------------------------------- /gj2ascii/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | _ ___ _ _ 3 | ____ _ (_)__ \ ____ ___________(_|_) 4 | / __ `/ / /__/ // __ `/ ___/ ___/ / / 5 | / /_/ / / // __// /_/ (__ ) /__/ / / 6 | \__, /_/ //____/\__,_/____/\___/_/_/ 7 | /____/___/ 8 | 9 | Render GeoJSON as ASCII with Python. 10 | 11 | >>> import fiona as fio 12 | >>> import gj2ascii 13 | >>> with fio.open('sample-data/polygons.geojson') as poly, \\ 14 | ... fio.open('sample-data/lines.geojson') as lines, \\ 15 | ... fio.open('sample-data/small-aoi-polygon-line.geojson') as bbox: 16 | ... charmap = [ 17 | ... (poly, '1'), 18 | ... (lines, '0') 19 | ... ] 20 | ... print(gj2ascii.style_multiple(charmap, 40, fill=' ', bbox=bbox.bounds)) 21 | 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 2 2 2 2 22 | 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 2 2 2 23 | 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 24 | 0 0 0 0 1 0 0 0 0 0 0 0 0 1 1 1 1 1 0 2 25 | 2 2 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 26 | 2 2 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 27 | 2 1 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 28 | 2 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 29 | 1 2 0 0 0 0 0 0 1 0 0 0 0 0 2 0 0 0 0 0 30 | 0 1 0 0 0 0 0 0 1 1 0 0 0 2 2 0 0 0 0 0 31 | 0 0 1 0 0 0 0 0 1 0 1 0 2 2 2 0 0 0 0 0 32 | 0 0 0 1 0 0 0 0 1 0 0 1 2 2 2 0 0 0 0 0 33 | 0 0 0 0 1 0 0 1 0 0 2 2 1 2 2 0 0 0 0 0 34 | 0 0 0 0 0 1 0 1 0 2 2 2 2 1 2 0 0 0 0 0 35 | 0 0 0 0 0 0 1 1 2 2 2 2 2 2 1 0 0 0 0 0 36 | """ 37 | 38 | 39 | from .core import ( 40 | array2ascii, ascii2array, dict2table, min_bbox, paginate, render, 41 | render_multiple, stack, style, style_multiple 42 | ) 43 | 44 | from .core import ( 45 | DEFAULT_WIDTH, DEFAULT_FILL, DEFAULT_CHAR, DEFAULT_CHAR_RAMP, 46 | DEFAULT_CHAR_COLOR, DEFAULT_COLOR_CHAR, ANSI_COLORMAP 47 | ) 48 | 49 | 50 | __version__ = '0.4.1' 51 | __author__ = 'Kevin Wurster' 52 | __email__ = 'wursterk@gmail.com' 53 | __source__ = 'https://github.com/geowurster/gj2ascii' 54 | __license__ = ''' 55 | New BSD License 56 | 57 | Copyright (c) 2015, Kevin D. Wurster 58 | All rights reserved. 59 | 60 | Redistribution and use in source and binary forms, with or without 61 | modification, are permitted provided that the following conditions are met: 62 | 63 | * Redistributions of source code must retain the above copyright notice, this 64 | list of conditions and the following disclaimer. 65 | 66 | * Redistributions in binary form must reproduce the above copyright notice, 67 | this list of conditions and the following disclaimer in the documentation 68 | and/or other materials provided with the distribution. 69 | 70 | * The names of its contributors may not be used to endorse or promote products 71 | derived from this software without specific prior written permission. 72 | 73 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 74 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 75 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 76 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 77 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 78 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 79 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 80 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 81 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 82 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 83 | ''' 84 | -------------------------------------------------------------------------------- /gj2ascii/cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | Commandline interface for gj2ascii 3 | 4 | See `gj2ascii --help` for more info. 5 | 6 | $ cat sample-data/polygons.geojson | gj2ascii - \\ 7 | sample-data/lines.geojson \\ 8 | --bbox sample-data/small-aoi-polygon-line.geojson \\ 9 | --width 40 \\ 10 | --char \^=red \\ 11 | --char -=blue \\ 12 | --fill .=green 13 | . . . . . . - . . . . . . . . . ^ ^ ^ ^ 14 | . . . . . - . . . . . . . . . . . ^ ^ ^ 15 | . . . . - . . . . . . . . . . . . . - - 16 | . . . . - . . . . . . . . - - - - - . ^ 17 | ^ ^ . - . . . . . . . . . . . . . . . . 18 | ^ ^ - . . . . . . . . . . . . . . . . . 19 | ^ - ^ . . . . . . . . . . . . . . . . . 20 | ^ - . . . . . . . . . . . . . . . . . . 21 | - ^ . . . . . . - . . . . . ^ . . . . . 22 | . - . . . . . . - - . . . ^ ^ . . . . . 23 | . . - . . . . . - . - . ^ ^ ^ . . . . . 24 | . . . - . . . . - . . - ^ ^ ^ . . . . . 25 | . . . . - . . - . . ^ ^ - ^ ^ . . . . . 26 | . . . . . - . - . ^ ^ ^ ^ - ^ . . . . . 27 | . . . . . . - - ^ ^ ^ ^ ^ ^ - . . . . . 28 | """ 29 | 30 | 31 | import itertools 32 | import os 33 | import random 34 | import string 35 | 36 | import gj2ascii 37 | from .pycompat import zip_longest 38 | from .pycompat import string_types 39 | 40 | import click 41 | import fiona as fio 42 | try: # pragma no cover 43 | import emoji 44 | except ImportError: # pragma no cover 45 | emoji = None 46 | 47 | 48 | def _build_colormap(c_map, f_map): 49 | 50 | """ 51 | Combine a character and fill map into a single colormap 52 | """ 53 | 54 | return {k: v for k, v in c_map + f_map if v is not None} 55 | 56 | 57 | def _cb_char_and_fill(ctx, param, value): 58 | 59 | """ 60 | Click callback to validate --fill and --char. --char supports multiple 61 | syntaxes but they cannot be mixed: +, blue, +=blue. 62 | 63 | This whole thing works but needs to be cleaned up. 64 | """ 65 | 66 | # Good god this is gross ... 67 | 68 | # If the user didn't supply anything then value=None 69 | if value is None: 70 | value = () 71 | elif isinstance(value, string_types): 72 | value = value, 73 | else: 74 | value = value 75 | 76 | output = [] 77 | for val in value: 78 | if len(val) is 1: 79 | output.append((val, None)) 80 | elif '=' in val: 81 | output.append(val.rsplit('=', 1)) 82 | elif len(val) >= 3 and val.startswith(':') and val.endswith(':'): 83 | output.append('EMOJI-' + val) 84 | if emoji is None: # pragma no cover 85 | raise click.BadParameter('detected an emoji but could not import the emoji ' 86 | 'library. Please `pip install emoji`.') 87 | else: 88 | try: 89 | char = gj2ascii.DEFAULT_COLOR_CHAR[val] 90 | except KeyError: 91 | raise click.BadParameter( 92 | "must be a single character, color, or :emoji:, not: `{val}'.".format( 93 | val=val) 94 | ) 95 | output.append((char, val)) 96 | all_chars = list(itertools.chain(*output))[0::2] 97 | for idx, pair in enumerate(output): 98 | if isinstance(pair, string_types) and pair.startswith('EMOJI'): 99 | val = pair.replace('EMOJI-', '') 100 | char = random.choice(string.ascii_letters) 101 | _idx = 0 102 | while char in list(itertools.chain(*output))[::2]: # pragma no cover 103 | char = random.choice(string.ascii_letters) 104 | _idx += 1 105 | if _idx > len(string.ascii_letters): 106 | raise click.ClickException("Too many layers. The limit is probably 26. " 107 | "I'm not really sure...") 108 | output[idx] = (char, val) 109 | 110 | return output 111 | 112 | 113 | def _cb_properties(ctx, param, value): 114 | 115 | """ 116 | Click callback to validate --properties. 117 | """ 118 | 119 | if value in ('%all', None): 120 | return value 121 | else: 122 | return value.split(',') 123 | 124 | 125 | def _cb_multiple_default(ctx, param, value): 126 | 127 | """ 128 | Click callback. 129 | 130 | Options that can be specified multiple times are an empty tuple if they are 131 | never specified. This callback wraps the default value in a tuple if the 132 | argument is not specified at all. 133 | """ 134 | 135 | if len(value) is 0: 136 | return param.default, 137 | else: 138 | return value 139 | 140 | 141 | def _cb_bbox(ctx, param, value): 142 | 143 | """ 144 | Validate --bbox by making sure it doesn't self-intersect. 145 | """ 146 | 147 | if value: 148 | x_min, y_min, x_max, y_max = value 149 | if (x_min > x_max) or (y_min > y_max): 150 | raise click.BadParameter( 151 | 'self-intersection: {bbox}'.format(bbox=value)) 152 | 153 | return value 154 | 155 | 156 | def _cb_infile(ctx, param, value): 157 | 158 | """ 159 | Click callback to validate infile. Let the user specify a datasource and its 160 | layers in a single argument. 161 | 162 | Example usage: 163 | 164 | Render all layers in a datasource 165 | $ gj2ascii sample-data/multilayer-polygon-line 166 | 167 | Render layer with name 'polygons' in a multilayer datasource 168 | $ gj2ascii sample-data/multilayer-polygon-line,polygons 169 | 170 | # Render two layers in a specific order in a multilayer datasource 171 | $ gj2ascii sample-data/multilayer-polygon-line,lines,polygons 172 | 173 | # Render layers from multiple files 174 | $ gj2ascii sample-data/polygons.geojson sample-data/multilayer-polygon-line,lines 175 | 176 | Example output: 177 | 178 | [('sample-data/multilayer-polygon-line', ['polygons', 'lines'])] 179 | 180 | [('sample-data/multilayer-polygon-line', ['polygons'])] 181 | 182 | [('sample-data/multilayer-polygon-line', ['lines', 'polygons'])] 183 | 184 | [ 185 | ('sample-data/polygons.geojson', ['polygons']), 186 | ('sample-data/multilayer-polygon-line', ['lines']) 187 | ] 188 | 189 | Returns 190 | ------- 191 | list 192 | A list of tuples where the first element of each tuple is the datasource 193 | and the second is a list of layers to render. 194 | """ 195 | 196 | output = [] 197 | for ds_layers in value: 198 | _split = ds_layers.split(',') 199 | ds = _split[0] 200 | layers = _split[1:] 201 | if ds != '-' and (len(layers) is 0 or '%all' in layers): 202 | layers = fio.listlayers(ds) 203 | elif ds == '-': 204 | layers = [None] 205 | output.append((ds, layers)) 206 | 207 | return output 208 | 209 | 210 | def _cb_print_colors(ctx, param, value): 211 | 212 | """ 213 | Click callback to print available colors to the commandline and then exit. 214 | """ 215 | 216 | if not value or ctx.resilient_parsing: 217 | return 218 | for color in gj2ascii.DEFAULT_COLOR_CHAR.keys(): 219 | click.echo(color) 220 | ctx.exit() 221 | 222 | 223 | @click.command() 224 | @click.version_option(version=gj2ascii.__version__) 225 | @click.argument('infile', nargs=-1, required=True, callback=_cb_infile) 226 | @click.option( 227 | '-o', '--outfile', type=click.File(mode='w'), default='-', 228 | help="Write to an output file instead of stdout." 229 | ) 230 | @click.option( 231 | '-w', '--width', type=click.INT, default=gj2ascii.DEFAULT_WIDTH, 232 | help="Render across N text columns. Height is auto-computed." 233 | ) 234 | @click.option( 235 | '--iterate', is_flag=True, 236 | help="Iterate over input features and display each individually." 237 | ) 238 | @click.option( 239 | '-f', '--fill', 'fill_map', metavar='CHAR', default=gj2ascii.DEFAULT_FILL, 240 | callback=_cb_char_and_fill, 241 | help="Single character to use for areas that are not covered by a geometry." 242 | ) 243 | @click.option( 244 | '-c', '--char', 'char_map', metavar='CHAR', multiple=True, callback=_cb_char_and_fill, 245 | help="Character to use for areas that intersect a geometry. Several syntaxes are " 246 | "supported: `-c +`, `-c blue`, `-c +=blue`, and `-c :emoji:`. The first will use + " 247 | "and no color, the second will select a character in the range 0 to 9 and produce " 248 | "blue geometries, the third will use + and produce blue geometry, and the last will " 249 | "use the specified emoji." 250 | ) 251 | @click.option( 252 | '--all-touched', is_flag=True, multiple=True, default=False, 253 | callback=_cb_multiple_default, 254 | help="When rasterizing geometries fill any pixel that touches a geometry rather than any " 255 | "pixel whose center is within a geometry." 256 | ) 257 | @click.option( 258 | '--crs', 'crs_def', metavar='DEF', multiple=True, callback=_cb_multiple_default, 259 | help="Specify input CRS. No transformations are performed but this will override the " 260 | "input CRS or assign a new one." 261 | ) 262 | @click.option( 263 | '--no-prompt', is_flag=True, 264 | help="Print all geometries without pausing in between." 265 | ) 266 | @click.option( 267 | '-p', '--properties', metavar='NAME,NAME,...', callback=_cb_properties, 268 | help="When iterating over features display the specified fields above each geometry. Use " 269 | "`%all` for all." 270 | ) 271 | @click.option( 272 | '--bbox', nargs=4, type=click.FLOAT, metavar="X_MIN Y_MIN X_MAX Y_MAX", callback=_cb_bbox, 273 | help="Only render data within the bounding box. If not specified a minimum bounding box " 274 | "will be computed from all input layers, which can be expensive for layers with a " 275 | "large number of features." 276 | ) 277 | @click.option( 278 | '--no-style', is_flag=True, 279 | help="Disable colors and emoji even if they are specified with `--char`. Emoji will be " 280 | "displayed as a single random character." 281 | ) 282 | @click.option( 283 | '--colors', is_flag=True, callback=_cb_print_colors, expose_value=False, is_eager=True, 284 | help="Print a list of available colors and exit." 285 | ) 286 | def main(infile, outfile, width, iterate, fill_map, char_map, all_touched, crs_def, no_prompt, 287 | properties, bbox, no_style): 288 | 289 | """ 290 | Render spatial vector data as ASCII with colors and emoji. 291 | 292 | \b 293 | Multiple layers from multiple files can be rendered as characters, colors, 294 | or emoji. When working with multiple layers they are rendered in order 295 | according to the painters algorithm. 296 | 297 | \b 298 | When working with multiple layers the layer specific arguments 299 | can be specified once to apply to all or multiple to apply specific conditions 300 | to specific layers. In the latter case the arguments are applied in order so 301 | the second instance of an argument corresponds to the second layer. 302 | 303 | \b 304 | Examples: 305 | \b 306 | Render the entire layer across 40 text columms: 307 | \b 308 | $ gj2ascii ${INFILE} --width 40 309 | \b 310 | Read from stdin and fill all pixels that intersect a geometry: 311 | \b 312 | $ cat ${INFILE} | gj2ascii - \\ 313 | --width 30 \\ 314 | --all-touched 315 | \b 316 | Render individual features across 20 columns and display the attributes 317 | for two fields: 318 | \b 319 | $ gj2ascii ${INFILE} \\ 320 | --properties ${PROP1},${PROP2} \\ 321 | --width 20 \\ 322 | --iterate 323 | \b 324 | Render multiple layers. The first layer is read from stdin and will be 325 | rendered as a single character, the second will be the character `*' but 326 | masked by the color blue, the third will be a randomly assigned character 327 | but masked by the color black, and the fourth will be the :thumbsup: emoji: 328 | \b 329 | $ cat ${SINGLE_LAYER_INFILE} | gj2ascii \\ 330 | - \\ 331 | ${INFILE_2},${LAYER_NAME_1},${LAYER_NAME_2} \\ 332 | ${SINGLE_LAYER_INFILE_1} \\ 333 | --char + \\ 334 | --char \\*=blue \\ 335 | --char black \\ 336 | --char :thumbsup: 337 | """ 338 | 339 | fill_char = [c[0] for c in fill_map][-1] 340 | num_layers = sum([len(layers) for ds, layers in infile]) 341 | 342 | # ==== Render individual features ==== # 343 | if iterate: 344 | 345 | if num_layers > 1: 346 | raise click.ClickException( 347 | "Can only iterate over a single layer. Specify only one infile for " 348 | "single-layer datasources and `INFILE,LAYERNAME` for multi-layer datasources.") 349 | 350 | if len(infile) > 1 or num_layers > 1 or len(crs_def) > 1 \ 351 | or len(char_map) > 1 or len(all_touched) > 1: 352 | raise click.ClickException( 353 | "Can only iterate over 1 layer - all layer-specific arguments can only be " 354 | "specified once each.") 355 | 356 | # User is writing to an output file. Don't prompt for next feature every time 357 | # TODO: Don't just compare to '' but sys.stdout.name throws an exception 358 | if not no_prompt and hasattr(outfile, 'name') and outfile.name != '': 359 | no_prompt = True 360 | 361 | if not char_map: 362 | char_map = [(gj2ascii.DEFAULT_CHAR, None)] 363 | 364 | # The odd list slicing is due to infile looking something like this: 365 | # [ 366 | # ('sample-data/polygons.geojson', ['polygons']), 367 | # ('sample-data/multilayer-polygon-line', ['lines', 'polygons']) 368 | # ] 369 | if num_layers > 0: 370 | layer = infile[-1][1][-1] 371 | else: 372 | layer = None 373 | in_ds = infile[-1][0] 374 | if in_ds == '-' and not no_prompt: 375 | 376 | # This exception blocks a feature - see the content for more info. 377 | raise click.ClickException( 378 | "Unfortunately features cannot yet be directly iterated over when reading " 379 | "from stdin. The simplest workaround is to use:" + os.linesep * 2 + 380 | " $ cat data.geojson | gj2ascii - --iterate --no-prompt | more" + 381 | os.linesep * 2 + 382 | "This issue has been logged: https://github.com/geowurster/gj2ascii/issues/25" 383 | ) 384 | with fio.open(infile[-1][0], layer=layer, crs=crs_def[-1]) as src: 385 | 386 | if properties == '%all': 387 | properties = src.schema['properties'].keys() 388 | 389 | # Get the last specified parameter when possible in case there's a bug in the 390 | # validation above. 391 | kwargs = { 392 | 'width': width, 393 | 'char': [_c[0] for _c in char_map][-1], 394 | 'fill': fill_char, 395 | 'properties': properties, 396 | 'all_touched': all_touched[-1], 397 | 'bbox': bbox, 398 | 'colormap': _build_colormap(char_map, fill_map) 399 | } 400 | if no_style: 401 | kwargs['colormap'] = None 402 | 403 | for feature in gj2ascii.paginate(src.filter(bbox=bbox), **kwargs): 404 | click.echo(feature, file=outfile) 405 | if not no_prompt and click.prompt( 406 | "Press enter for next feature or 'q + enter' to exit", 407 | default='', show_default=False, err=True) \ 408 | not in ('', os.linesep): # pragma no cover 409 | raise click.Abort() 410 | 411 | # ==== Render all input layers ==== # 412 | else: 413 | 414 | # if len(crs_def) not in (1, num_layers) or len(char_map) not in (0, 1, num_layers) \ 415 | # or len(all_touched) not in (1, num_layers) 416 | 417 | # User didn't specify any characters/colors but the number of input layers exceeds 418 | # the number of available colors. 419 | if not char_map: 420 | if num_layers > len(gj2ascii.ANSI_COLORMAP): 421 | raise click.ClickException( 422 | "can't auto-generate color ramp - number of input layers exceeds number " 423 | "of colors. Specify one `--char` per layer.") 424 | elif num_layers is 1: 425 | char_map = [(gj2ascii.DEFAULT_CHAR, None)] 426 | else: 427 | char_map = [(str(_i), gj2ascii.DEFAULT_CHAR_COLOR[str(_i)]) 428 | for _i in range(num_layers)] 429 | elif len(char_map) is not num_layers: 430 | raise click.ClickException( 431 | "Number of `--char` arguments must equal the number of layers being " 432 | "processed. Found %s characters and %s layers. Characters and colors will " 433 | "be generated if none are supplied." % (len(char_map), num_layers)) 434 | if not char_map: 435 | char_map = {gj2ascii.DEFAULT_CHAR: None} 436 | 437 | # User didn't specify a bounding box. Compute the minimum bbox for all layers. 438 | if not bbox: 439 | coords = [] 440 | for ds, layer_names in infile: 441 | for layer, crs in zip_longest(layer_names, crs_def): 442 | with fio.open(ds, layer=layer, crs=crs) as src: 443 | coords += list(src.bounds) 444 | bbox = (min(coords[0::4]), min(coords[1::4]), max(coords[2::4]), max(coords[3::4])) 445 | 446 | # Render everything 447 | rendered_layers = [] 448 | overall_lyr_idx = 0 449 | for ds, layer_names in infile: 450 | for layer, crs, at in zip_longest(layer_names, crs_def, all_touched): 451 | char = [_c[0] for _c in char_map][overall_lyr_idx] 452 | overall_lyr_idx += 1 453 | with fio.open(ds, layer=layer, crs=crs) as src: 454 | rendered_layers.append( 455 | # Layers will be stacked, which requires fill to be set to a space 456 | gj2ascii.render( 457 | src, width=width, fill=' ', char=char, all_touched=at, bbox=bbox)) 458 | 459 | stacked = gj2ascii.stack(rendered_layers, fill=fill_char) 460 | if no_style: 461 | styled = stacked 462 | else: 463 | styled = gj2ascii.style(stacked, stylemap=_build_colormap(char_map, fill_map)) 464 | click.echo(styled, file=outfile) 465 | -------------------------------------------------------------------------------- /gj2ascii/core.py: -------------------------------------------------------------------------------- 1 | """ 2 | Core components for gj2ascii 3 | """ 4 | 5 | 6 | from __future__ import division 7 | 8 | from collections import OrderedDict 9 | import itertools 10 | import math 11 | import os 12 | from types import GeneratorType 13 | 14 | from .pycompat import text_type 15 | 16 | import affine 17 | import numpy as np 18 | import rasterio as rio 19 | from rasterio.features import rasterize 20 | from shapely.geometry import asShape 21 | from shapely.geometry import mapping 22 | try: # pragma no cover 23 | import emoji 24 | except ImportError: # pragma no cover 25 | emoji = None 26 | 27 | __all__ = [ 28 | 'render', 'stack', 'style', 'render_multiple', 'style_multiple', 'paginate', 'dict2table', 29 | 'ascii2array', 'array2ascii', 'min_bbox', 30 | 'DEFAULT_WIDTH', 'DEFAULT_FILL', 'DEFAULT_CHAR', 'DEFAULT_CHAR_RAMP', 'DEFAULT_CHAR_COLOR', 31 | 'DEFAULT_COLOR_CHAR', 'ANSI_COLORMAP', 32 | ] 33 | 34 | 35 | DEFAULT_FILL = ' ' 36 | DEFAULT_CHAR = '+' 37 | DEFAULT_WIDTH = 80 38 | DEFAULT_CHAR_RAMP = [ 39 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '#', '@', '0', '=', '-', '%', '$'] 40 | _ANSI_RESET = '\033[0m' 41 | ANSI_COLORMAP = { 42 | 'black': '\x1b[30m\x1b[40m', 43 | 'red': '\x1b[31m\x1b[41m', 44 | 'green': '\x1b[32m\x1b[42m', 45 | 'yellow': '\x1b[33m\x1b[43m', 46 | 'blue': '\x1b[34m\x1b[44m', 47 | 'magenta': '\x1b[35m\x1b[45m', 48 | 'cyan': '\x1b[36m\x1b[46m', 49 | 'white': '\x1b[37m\x1b[47m' 50 | } 51 | DEFAULT_CHAR_COLOR = { 52 | '0': 'green', 53 | '1': 'blue', 54 | '2': 'red', 55 | '3': 'yellow', 56 | '4': 'cyan', 57 | '5': 'magenta', 58 | '6': 'white', 59 | '7': 'black' 60 | } 61 | DEFAULT_COLOR_CHAR = {v: k for k, v in DEFAULT_CHAR_COLOR.items()} 62 | 63 | 64 | def dict2table(dictionary): 65 | 66 | """ 67 | Convert a dictionary to an ASCII formatted table. 68 | 69 | Example: 70 | 71 | >>> import gj2ascii 72 | >>> example_dict = OrderedDict(( 73 | ... ('ALAND', '883338808'), 74 | ... ('AWATER', 639183), 75 | ... ('CBSAFP', None), 76 | ... ('CLASSFP', 'H1'), 77 | ... ('COUNTYFP', '001') 78 | ... )) 79 | >>> print(gj2ascii.dict2table(example_dict)) 80 | +----------+-----------+ 81 | | AWATER | 4639183 | 82 | | ALAND | 883338808 | 83 | | COUNTYFP | 001 | 84 | | CLASSFP | H1 | 85 | | CBSAFP | None | 86 | +----------+-----------+ 87 | 88 | 89 | Parameter 90 | --------- 91 | dictionary : dict 92 | Keys are rendered in the first column left justified and values are 93 | rendered in the second right justified. 94 | 95 | 96 | Returns 97 | ------- 98 | str 99 | """ 100 | 101 | if not dictionary: 102 | raise ValueError("Cannot format table - input dictionary is empty.") 103 | 104 | # Cast everything to a string now so we don't have to do it again later 105 | dictionary = OrderedDict(((text_type(k), text_type(v)) for k, v in dictionary.items())) 106 | 107 | # Add two at the end to account for spaces around | 108 | prop_width = max([len(e) for e in dictionary.keys()]) 109 | value_width = max([len(e) for e in dictionary.values()]) 110 | 111 | # Add 2 to the prop/value width to account for the single space padding around the 112 | # properties and values 113 | # +----------+-------+ 114 | # | Property | Value | 115 | # ^ ^ ^ ^ 116 | # We don't need it later so only add it here 117 | divider = ''.join(['+', '-' * (prop_width + 2), '+', '-' * (value_width + 2), '+']) 118 | output = [divider] 119 | for prop, value in dictionary.items(): 120 | value = text_type(value) 121 | prop_content = prop + ' ' * (prop_width - len(prop)) 122 | value_content = ' ' * (value_width - len(value)) + value 123 | output.append('| ' + prop_content + ' | ' + value_content + ' |') 124 | 125 | # Add trailing divider 126 | output.append(divider) 127 | 128 | return os.linesep.join(output) 129 | 130 | 131 | def _geometry_extractor(ftrz): 132 | 133 | """ 134 | A generator that yields GeoJSON geometry objects extracted from various 135 | data formats and containers. 136 | 137 | 138 | Parameters 139 | ---------- 140 | ftrz : dict or iterator 141 | Can be a single GeoJSON feature, geometry, object with a `__geo_interface__` 142 | method, or an iterable producing one of those types per iteration. 143 | 144 | 145 | Yields 146 | ------ 147 | dict 148 | A GeoJSON geometry. 149 | 150 | 151 | Raises 152 | ------ 153 | TypeError 154 | Geometry could not be extracted from an input object. 155 | """ 156 | 157 | if isinstance(ftrz, dict) or hasattr(ftrz, '__geo_interface__'): 158 | ftrz = [ftrz] 159 | for obj in ftrz: 160 | if hasattr(obj, '__geo_interface__'): 161 | obj = mapping(obj) 162 | if obj['type'] == 'Feature': 163 | yield obj['geometry'] 164 | elif 'coordinates' in obj: 165 | yield obj 166 | else: 167 | raise TypeError( 168 | "An input object isn't a feature, geometry, or object supporting " 169 | "__geo_interface__: %s" % obj) 170 | 171 | 172 | def ascii2array(ascii): 173 | 174 | """ 175 | Convert an ASCII rendering to an array. The returned object is not a numpy 176 | array but can easily be converted with `np.array()`. 177 | 178 | Example input: 179 | 180 | # Rendered ASCII 181 | * * * * * 182 | * * 183 | * * * * * 184 | 185 | >>> import gj2ascii 186 | >>> pprint(gj2ascii.ascii2array(rendered_ascii)) 187 | [['*', '*', '*', '*', '*'], 188 | [' ', '*', ' ', '*', ' '], 189 | ['*', '*', '*', '*', '*']] 190 | 191 | 192 | Parameters 193 | --------- 194 | ascii : str 195 | Rendered ASCII from `render()` or `stack()`. 196 | 197 | 198 | Returns 199 | ------- 200 | list 201 | A list where each element is a list containing one value per pixel. 202 | """ 203 | 204 | return [list(row[::2]) for row in ascii.splitlines()] 205 | 206 | 207 | def array2ascii(arr): 208 | 209 | """ 210 | Convert an array to its ASCII rendering. Designed to take the output from 211 | `ascii2array()` but will also work on a numpy array. 212 | 213 | Example input: 214 | 215 | # Array 216 | [['*', '*', '*', '*', '*'], 217 | [' ', '*', ' ', '*', ' '], 218 | ['*', '*', '*', '*', '*']] 219 | 220 | >>> import gj2ascii 221 | >>> print(gj2ascii.ascii2array(array)) 222 | * * * * * 223 | * * 224 | * * * * * 225 | 226 | 227 | Parameters 228 | ---------- 229 | arr : str 230 | An array with a structure similar to the output of `ascii2array()`. 231 | 232 | 233 | Returns 234 | ------- 235 | list 236 | A block of ASCII text similar to the output of `render()`. 237 | """ 238 | 239 | return os.linesep.join([' '.join(row) for row in arr]) 240 | 241 | 242 | def stack(rendered_items, fill=DEFAULT_FILL): 243 | 244 | """ 245 | Combine multiple overlapping renderings into a single rendered product. All 246 | input renderings must have been rendered with the same width, bbox, and use 247 | a single space for the fill value. Overlapping geometries are merged with 248 | the painters algorithm and pixels containing a single space are considered 249 | transparent. Use the `fill` parameter to specify an output fill character. 250 | 251 | It is important that all input layers be rendered with the same width and 252 | bbox to ensure that they actually represent the same spatial area. It is 253 | possible to provide input layers whose ASCII representations have a matching 254 | number of rows and columns even if they do not overlap spatially or were 255 | produced from different bounding boxes. The example below shows how to 256 | ensure proper stackability. 257 | 258 | Example: 259 | 260 | # Rendered geometry 1 261 | 0 0 0 0 0 262 | 0 263 | 0 0 0 0 0 264 | 265 | # Rendered geometry 2 266 | 1 1 267 | 268 | 1 1 269 | 270 | # Define a bounding box 271 | bbox = (-10, -15, 10, 15) 272 | 273 | >>> import gj2ascii 274 | >>> layer1 = gj2ascii.render( 275 | ... geom1, width=5, fill=' ', value='0', bbox=bbox) 276 | >>> layer2 = gj2ascii.render( 277 | ... geom2, width=5, fill=' ', value='1', bbox=bbox) 278 | >>> layers = [layer1, layer2] 279 | >>> print(gj2ascii.stack(layers, fill='.')) 280 | 1 0 0 0 1 281 | . . 0 . . 282 | 1 0 0 0 1 283 | 284 | Parameters 285 | ---------- 286 | rendered_items : iterable 287 | An iterable producing one rendered layer per iteration. Layers must 288 | all have the same dimension and must have been rendered with an empty 289 | space ` ' as the fill value. Using the same `bbox` and `width` values 290 | for `render()` when preparing input layers helps ensure layers have 291 | matching dimensions. 292 | 293 | fill : str, optional 294 | A new fill value for the rendered stack. Must be a single character. 295 | 296 | 297 | Raises 298 | ------ 299 | ValueError 300 | Fill value is too long or input ASCII has heterogeneous dimensions. 301 | 302 | 303 | Returns 304 | ------- 305 | str 306 | All stacked layers rendered into a single ASCII representation with the 307 | first input layer on the bottom and the last on top. 308 | """ 309 | 310 | fill = str(fill) 311 | if len(fill) is not 1: 312 | raise ValueError("Invalid fill value `%s' - must be 1 character long" % fill) 313 | 314 | output_array = [] 315 | for row_stack in zip(*map(ascii2array, rendered_items)): 316 | 317 | if len(set((len(_r) for _r in row_stack))) is not 1: 318 | raise ValueError("Input layers have heterogeneous dimensions") 319 | 320 | o_row = [] 321 | for pixel_stack in zip(*row_stack): 322 | opaque_pixels = [_p for _p in pixel_stack if _p != ' '] 323 | if len(opaque_pixels) is 0: 324 | o_row.append(fill) 325 | else: 326 | o_row.append(opaque_pixels[-1]) 327 | output_array.append(o_row) 328 | 329 | return array2ascii(output_array) 330 | 331 | 332 | def render(ftrz, width=DEFAULT_WIDTH, fill=DEFAULT_FILL, char=DEFAULT_CHAR, 333 | all_touched=False, bbox=None): 334 | 335 | """ 336 | Render GeoJSON features, geometries, or objects supporting `__geo_interface__` 337 | as ASCII. 338 | 339 | Render an entire layer: 340 | 341 | >>> import gj2ascii 342 | >>> import fiona 343 | >>> with fiona.open('sample-data/polygons.geojson') as src: 344 | ... print(gj2ascii.render(src, 15, fill='.', value='*')) 345 | . * . * . . . . . . . . . . . 346 | . * * . . . . . . * . . . . . 347 | . . . . . . . . . . . . . . . 348 | . . . . . . . . . . . . . . . 349 | . . . . . . . * * . . . . . . 350 | . . . . . . . * * * . . . . . 351 | . . . . . . . . * * . . . * . 352 | * * * . . . . . . . . . * * * 353 | . * * . . . . . . . . . . * * 354 | . . . . . . * . . . . . . * . 355 | . . . . . * * . . . . . . . . 356 | . . . . . * * . . . . . . . . 357 | . . . . . . * . . . . . . . . 358 | 359 | Render a single feature: 360 | 361 | >>> import gj2ascii 362 | >>> import fiona 363 | >>> with fiona.open('sample-data/polygons.geojson') as src: 364 | ... print(gj2ascii.render(next(src), 15, fill='.', value='*')) 365 | + 366 | + + 367 | + + + + + 368 | + + + + + + + + 369 | + + + + + + + + + 370 | + + + + + + + + + + + 371 | + + + + + + + + + + + + 372 | + + + + + + + + + + 373 | + + + + + + + 374 | + + + 375 | 376 | 377 | Parameters 378 | ---------- 379 | ftrz : dict or iterator 380 | Can be a single GeoJSON feature, geometry, object supporting 381 | `__geo_interface__`, or an iterable producing one of those types per 382 | iteration. 383 | 384 | width : int, optional 385 | Render across N text columns. Height is auto-computed. 386 | 387 | char : str or None, optional 388 | Single character to use for pixels intersecting a geometry. 389 | 390 | fill : str or None, optional 391 | Single character to use for pixels that are not touched by a geometry. 392 | 393 | all_touched : bool, optional 394 | Fill every pixel the geometries touch instead of every pixel whose 395 | center intersects the geometry. See `rasterio.features.rasterize()` 396 | for more information. 397 | 398 | bbox : tuple, optional 399 | A 4 element tuple consisting of x_min, y_min, x_max, y_max. Used to 400 | zoom in on a particular area of interest or to ensure that independent 401 | renderings can be stacked. If not supplied it is computed on the 402 | fly from all input objects. If reading from a datasource with a large 403 | number of features it is advantageous to supply this parameter to avoid 404 | a potentially large in-memory object and expensive computation. If not 405 | supplied and the input object has a `bounds` property, that value will 406 | be used. 407 | 408 | 409 | Raises 410 | ------ 411 | ValueError 412 | A parameter has an invalid value. 413 | 414 | Returns 415 | ------- 416 | str 417 | ASCII representation of input features or array. 418 | """ 419 | 420 | # User defines width as number of text columns but we need it as number of pixel 421 | # columns. One space is inserted between every pixel so divide by 2. 422 | width = int(math.ceil(width / 2)) 423 | 424 | # Values that aren't a string or 1 character wide cause rendering issues 425 | fill = str(fill) 426 | char = str(char) 427 | if len(fill) is not 1: 428 | raise ValueError("Invalid fill value `%s' - must be 1 character long" % fill) 429 | if len(char) is not 1: 430 | raise ValueError("Invalid pixel value `%s' - must be 1 character long" % char) 431 | if width <= 0: 432 | raise ValueError("Invalid width `%s' - must be > 0" % width) 433 | 434 | # If the input is a generator and the min/max values were not supplied we have to compute 435 | # them from the features, but we need them again later and generators cannot be reset. 436 | # This potentially creates a large in-memory object so if processing an entire layer it is 437 | # best to explicitly define min/max, especially because its also faster. 438 | if bbox: 439 | x_min, y_min, x_max, y_max = bbox 440 | else: 441 | _bbox, ftrz = min_bbox(ftrz, return_iter=True) 442 | x_min, y_min, x_max, y_max = _bbox 443 | 444 | x_delta = x_max - x_min 445 | y_delta = y_max - y_min 446 | cell_size = x_delta / width 447 | height = int(y_delta / cell_size) 448 | if height is 0: 449 | height = 1 450 | 451 | output_array = rasterize( 452 | fill=0, 453 | default_value=1, 454 | shapes=(g for g in _geometry_extractor(ftrz)), 455 | out_shape=(height, width), 456 | transform=affine.Affine.from_gdal(*(x_min, cell_size, 0.0, y_max, 0.0, -cell_size)), 457 | all_touched=all_touched, 458 | dtype=rio.uint8 459 | ) 460 | 461 | # Convert to string dtype and do character replacements 462 | output_array = output_array.astype(np.str_) 463 | if fill is not None and fill != '0': 464 | output_array = np.char.replace(output_array, '0', fill) 465 | if char is not None and fill != '1': 466 | output_array = np.char.replace(output_array, '1', char) 467 | 468 | return array2ascii(output_array) 469 | 470 | 471 | def paginate(ftrz, width=DEFAULT_WIDTH, properties=None, colormap=None, **kwargs): 472 | 473 | """ 474 | Generator to create paginated output for individual features - also handles 475 | attribute table formatting via the `properties` argument. Primarily used 476 | by the CLI. 477 | 478 | 479 | Properties 480 | ---------- 481 | ftrz : dict or iterator 482 | Anything accepted by `render()`. 483 | 484 | properties : list, optional 485 | Display a table with the specified properties above the geometry. 486 | 487 | colormap : dict or None, optional 488 | If provided the output text will contain color codes or emoji. See 489 | `style()` for more information. 490 | 491 | kwargs : **kwargs, optional 492 | Additional keyword arguments for `render()`. 493 | 494 | 495 | Yields 496 | ------ 497 | str 498 | One feature (with attribute table and colors if specified) as ascii. 499 | """ 500 | 501 | for item in ftrz: 502 | 503 | output = [] 504 | 505 | if properties is not None: 506 | output.append( 507 | dict2table(OrderedDict((p, item['properties'][p]) for p in properties))) 508 | r = render(item, width=width, **kwargs) 509 | if not colormap: 510 | output.append(r) 511 | else: 512 | output.append(style(r, stylemap=colormap)) 513 | 514 | yield os.linesep.join(output) + os.linesep 515 | 516 | 517 | def style(rendered_ascii, stylemap): 518 | 519 | """ 520 | Colorize or add emoji to an ASCII rendering. 521 | 522 | 523 | Parameters 524 | ---------- 525 | rendered_ascii : str 526 | An ASCII rendering from `render()` or `stack()`. 527 | 528 | stylemap : dict 529 | A dictionary where keys are color or emoji names and values are characters 530 | to which the color will be applied. 531 | 532 | 533 | Returns 534 | ------- 535 | str 536 | A formatted string containing ANSI codes that is ready for `print()`. 537 | """ 538 | 539 | output = [] 540 | for row in ascii2array(rendered_ascii): 541 | o_row = [] 542 | for char in row: 543 | if char in stylemap: 544 | emoji_or_color = stylemap[char] 545 | if emoji_or_color in ANSI_COLORMAP: 546 | o_row.append(ANSI_COLORMAP[emoji_or_color] + char + ' ' + _ANSI_RESET) 547 | else: 548 | o_row.append(emoji.emojize(emoji_or_color + ' ', use_aliases=True)) 549 | else: 550 | o_row.append(char + ' ') 551 | output.append(''.join(o_row)) 552 | return os.linesep.join(output) 553 | 554 | 555 | def render_multiple(ftr_char_pairs, width=DEFAULT_WIDTH, fill=DEFAULT_FILL, **kwargs): 556 | 557 | """ 558 | A quick way to render multiple layers, features, or geometries without having 559 | to `render()` each layer and combine via `stack()`. See `style_multiple()` 560 | for similar functionality but with direct access to colors rather than 561 | characters. 562 | 563 | A minimum bounding box is computed from all the input objects if one is not 564 | supplied in the kwargs. See the 'bbox' parameter in `render()` for more 565 | information about how this parameter can be used and its performance 566 | implications. 567 | 568 | 569 | Example: 570 | 571 | >>> import gj2ascii 572 | >>> import fiona as fio 573 | >>> with fio.open('sample-data/polygons.geojson') as poly: 574 | ... with fio.open('sample-data/lines.json') as lines: 575 | ... print(gj2ascii.render_multiple([(poly, '0'), (lines, '1')], 10)) 576 | 0 577 | 1 1 1 1 1 578 | 1 1 579 | 1 0 0 1 580 | 0 1 1 1 1 1 0 581 | 0 1 1 1 0 582 | 1 1 1 1 583 | 1 0 1 584 | 585 | 586 | Parameters 587 | ---------- 588 | ftr_char_pairs : list 589 | A list of tuples where the first element of each tuple is an object 590 | suitable for `render()` and the second is the ASCII character to use 591 | when rendering. 592 | 593 | width : int, optional 594 | See `render()`. 595 | 596 | fill : str, optional 597 | See `render()`. 598 | 599 | kwargs : **kwargs, optional 600 | Additional keyword arguments for `render()`. 601 | 602 | 603 | Returns 604 | ------- 605 | str 606 | All input layers, features, or geometries rendered as ASCII. 607 | """ 608 | 609 | # Render is intended to be used as `render(ftrz, width)` but if it is expected to only be 610 | # supplied as a kwarg it might create some confusion. If the user supplies a value use 611 | # that instead. 612 | width = kwargs.pop('width', width) 613 | 614 | if 'bbox' in kwargs: 615 | bbox = kwargs.pop('bbox') 616 | iter_pairs = ftr_char_pairs 617 | else: 618 | coords = [] 619 | iter_pairs = [] 620 | for ftrz, char in ftr_char_pairs: 621 | _bbox, _ftrz_copy = min_bbox(ftrz, return_iter=True) 622 | coords.append(_bbox) 623 | iter_pairs.append((_ftrz_copy, char)) 624 | coords = [_i for _i in itertools.chain(*coords)] 625 | bbox = (min(coords[0::4]), min(coords[1::4]), max(coords[2::4]), max(coords[3::4])) 626 | 627 | rendered_layers = [] 628 | for ftrz, char in iter_pairs: 629 | rendered_layers.append( 630 | render(ftrz, width=width, char=char, bbox=bbox, fill=' ', **kwargs)) 631 | 632 | return stack(rendered_layers, fill=fill) 633 | 634 | 635 | def style_multiple(ftr_style_pairs, width=DEFAULT_WIDTH, fill=DEFAULT_FILL, **kwargs): 636 | 637 | """ 638 | A quick way to render and style multiple layers, features, or geometries 639 | without having to render each layer individually, combine via `stack()`, 640 | and apply colors/emoji via `style()`. See `render_multiple()` For similar 641 | functionality but with direct access to characters rather than colors. 642 | 643 | A minimum bounding box is computed from all the input objects if one is not 644 | supplied in the kwargs. See the 'bbox' parameter in `render()` for more 645 | information about how this parameter can be used and its performance 646 | implications. 647 | 648 | Example: 649 | 650 | # 7's in the output rendering are blue, 1's are black, and 0's are green 651 | >>> import gj2ascii 652 | >>> import fiona as fio 653 | >>> with fio.open('sample-data/polygons.geojson') as poly: 654 | ... with fio.open('sample-data/lines.geojson') as lines: 655 | ... input_layers = [(poly, 'blue'), (lines, 'black')] 656 | ... print(gj2ascii.style_multiple( 657 | ... input_layers, fill='green', width=10)) 658 | 0 0 0 0 0 0 1 0 0 0 659 | 0 7 7 7 7 0 7 0 0 0 660 | 0 0 7 0 0 0 7 0 0 0 661 | 0 0 0 7 0 1 1 7 0 0 662 | 1 0 7 0 7 7 7 7 0 1 663 | 1 7 0 7 0 0 0 0 7 1 664 | 0 0 7 7 7 0 0 0 7 0 665 | 0 0 7 1 7 0 0 0 0 0 666 | 667 | 668 | Parameters 669 | ---------- 670 | ftr_style_pairs : list 671 | A list of tuples where the first element of each tuple is an object 672 | suitable for `render()` and the second is the color or emoji name to 673 | apply to that object. 674 | 675 | fill : str or None, optional 676 | A color to use as the background color. If `None` then no color is 677 | applied. Input layers are assigned a character from 0 to 9 so selecting 678 | a fill value in this range will produce an output with a fill style that 679 | is inherited from one of the other layers because they both have the same 680 | character. This function exists to provide a quick way to look at 681 | multiple styled layers so to work around this issue use the other API 682 | components to build a more specific rendering. 683 | 684 | kwargs : **kwargs, optional 685 | Additional keyword arguments for `render_multiple()`. 686 | 687 | 688 | Returns 689 | ------- 690 | str 691 | All input layers, features, or geometries rendered, stacked, and styled. 692 | """ 693 | 694 | stylemap = {} 695 | 696 | if len(fill) is 1: 697 | fill_char = fill 698 | elif fill in DEFAULT_COLOR_CHAR: 699 | fill_char = DEFAULT_COLOR_CHAR[fill] 700 | stylemap[fill_char] = fill 701 | else: 702 | fill_char = DEFAULT_CHAR_RAMP[-1] 703 | stylemap[fill_char] = fill 704 | 705 | ftr_char_pairs = [] 706 | for idx, ftrs_style in enumerate(ftr_style_pairs): 707 | ftrs, styl = ftrs_style 708 | ftr_char_pairs.append((ftrs, str(idx))) 709 | stylemap[str(idx)] = styl 710 | 711 | return style( 712 | render_multiple(ftr_char_pairs, width=width, fill=fill_char, **kwargs), stylemap) 713 | 714 | 715 | def min_bbox(input_iter, return_iter=False): 716 | 717 | """ 718 | Compute a bbox from an iterable object containing features, geometries, or 719 | a feature collection. Both the bbox and a version of the iterator are 720 | returned in order to handle iterating over generators twice. The iterator 721 | itself is returned if it is not a generator. 722 | 723 | 724 | Parameters 725 | ---------- 726 | iterator : iterable object 727 | An iterable object returning one GeoJSON feature, or geometry, or 728 | object supporting `__geo_interface__` every iteration. 729 | 730 | return_iter : bool, optional 731 | Primarily used by the internal API. Instead of just returning the 732 | `bbox`, return (bbox, iter) where `iter` is a copy of the iterator 733 | if its a generator, otherwise the input iterator is returned. 734 | 735 | 736 | Returns 737 | ------- 738 | tuple 739 | (x_min, y_min, x_max, y_max) or ((x_min, y_min, x_max, y_max), ) 740 | """ 741 | 742 | if hasattr(input_iter, 'bounds'): 743 | bbox = input_iter.bounds 744 | output_iterator = input_iter 745 | else: 746 | if isinstance(input_iter, GeneratorType): 747 | coord_iter, output_iterator = itertools.tee(input_iter) 748 | else: 749 | coord_iter = input_iter 750 | output_iterator = input_iter 751 | 752 | coords = list( 753 | itertools.chain(*[asShape(g).bounds for g in _geometry_extractor(coord_iter)])) 754 | bbox = (min(coords[0::4]), min(coords[1::4]), max(coords[2::4]), max(coords[3::4])) 755 | 756 | if return_iter: 757 | return bbox, output_iterator 758 | else: 759 | return bbox 760 | -------------------------------------------------------------------------------- /gj2ascii/pycompat.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python 2/3 compatibility 3 | """ 4 | 5 | 6 | import itertools 7 | import sys 8 | 9 | 10 | if sys.version_info[0] >= 3: # pragma no cover 11 | string_types = str, 12 | text_type = str 13 | zip_longest = itertools.zip_longest 14 | else: # pragma no cover 15 | string_types = basestring, 16 | text_type = unicode 17 | zip_longest = itertools.izip_longest 18 | -------------------------------------------------------------------------------- /images/ascii-land-cover-80w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geowurster/gj2ascii/6f9736fd87ff98301022eed37d68989a78eb8cf8/images/ascii-land-cover-80w.png -------------------------------------------------------------------------------- /images/emoji-land-cover-80w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geowurster/gj2ascii/6f9736fd87ff98301022eed37d68989a78eb8cf8/images/emoji-land-cover-80w.png -------------------------------------------------------------------------------- /sample-data/bbox.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::26918" } }, 4 | 5 | "features": [ 6 | { "type": "Feature", "properties": { "MINX": 256407.04459707518, "MINY": 4366133.3803152731, "MAXX": 260185.46446841472, "MAXY": 4369446.9289331678, "CNTX": 258296.25453274493, "CNTY": 4367790.15462422, "AREA": 12519977.942503253, "PERIM": 14183.936978468613, "HEIGHT": 3313.5486178947613, "WIDTH": 3778.4198713395454 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 256407.044597075175261, 4366133.380315273068845 ], [ 256407.044597075175261, 4369446.92893316783011 ], [ 260185.464468414720614, 4369446.92893316783011 ], [ 260185.464468414720614, 4366133.380315273068845 ], [ 256407.044597075175261, 4366133.380315273068845 ] ] ] } } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /sample-data/lines.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::26918" } }, 4 | 5 | "features": [ 6 | { "type": "Feature", "properties": { }, "geometry": { "type": "LineString", "coordinates": [ [ 258079.368204057624098, 4369009.926701506599784 ], [ 256848.263169171346817, 4368880.953793089836836 ], [ 257622.100619671284221, 4368259.538870718330145 ], [ 256918.612028307688888, 4367274.654842809773982 ], [ 257540.026950678875437, 4366594.61587115842849 ], [ 257598.650999959179899, 4367274.654842809773982 ], [ 258243.51554204247077, 4366582.891061302274466 ] ] } }, 7 | { "type": "Feature", "properties": { }, "geometry": { "type": "LineString", "coordinates": [ [ 258993.903372830303852, 4368963.027462081983685 ], [ 259732.56639376207022, 4366911.185737271793187 ] ] } }, 8 | { "type": "Feature", "properties": { }, "geometry": { "type": "LineString", "coordinates": [ [ 258044.193774489453062, 4367731.922427196055651 ], [ 258923.554513693932677, 4367767.096856764517725 ] ] } } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /sample-data/multilayer-polygon-line/lines.dbf: -------------------------------------------------------------------------------- 1 | _A WFIDN 0 1 2 -------------------------------------------------------------------------------- /sample-data/multilayer-polygon-line/lines.prj: -------------------------------------------------------------------------------- 1 | PROJCS["NAD_1983_UTM_Zone_18N",GEOGCS["GCS_North_American_1983",DATUM["D_North_American_1983",SPHEROID["GRS_1980",6378137,298.257222101]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-75],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["Meter",1]] -------------------------------------------------------------------------------- /sample-data/multilayer-polygon-line/lines.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geowurster/gj2ascii/6f9736fd87ff98301022eed37d68989a78eb8cf8/sample-data/multilayer-polygon-line/lines.shp -------------------------------------------------------------------------------- /sample-data/multilayer-polygon-line/lines.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geowurster/gj2ascii/6f9736fd87ff98301022eed37d68989a78eb8cf8/sample-data/multilayer-polygon-line/lines.shx -------------------------------------------------------------------------------- /sample-data/multilayer-polygon-line/polygons.dbf: -------------------------------------------------------------------------------- 1 | _A WFIDN 0 1 2 3 4 5 -------------------------------------------------------------------------------- /sample-data/multilayer-polygon-line/polygons.prj: -------------------------------------------------------------------------------- 1 | PROJCS["NAD_1983_UTM_Zone_18N",GEOGCS["GCS_North_American_1983",DATUM["D_North_American_1983",SPHEROID["GRS_1980",6378137,298.257222101]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-75],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["Meter",1]] -------------------------------------------------------------------------------- /sample-data/multilayer-polygon-line/polygons.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geowurster/gj2ascii/6f9736fd87ff98301022eed37d68989a78eb8cf8/sample-data/multilayer-polygon-line/polygons.shp -------------------------------------------------------------------------------- /sample-data/multilayer-polygon-line/polygons.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geowurster/gj2ascii/6f9736fd87ff98301022eed37d68989a78eb8cf8/sample-data/multilayer-polygon-line/polygons.shx -------------------------------------------------------------------------------- /sample-data/points.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::26918" } }, 4 | 5 | "features": [ 6 | { "type": "Feature", "properties": { "ID": 0 }, "geometry": { "type": "Point", "coordinates": [ 259560.38826875956147, 4366988.610918885096908 ] } }, 7 | { "type": "Feature", "properties": { "ID": 1 }, "geometry": { "type": "Point", "coordinates": [ 259334.218662722414592, 4368027.355506312102079 ] } }, 8 | { "type": "Feature", "properties": { "ID": 2 }, "geometry": { "type": "Point", "coordinates": [ 257347.384467207622947, 4368171.645685196854174 ] } }, 9 | { "type": "Feature", "properties": { "ID": 3 }, "geometry": { "type": "Point", "coordinates": [ 257650.575321973708924, 4367061.294301466085017 ] } }, 10 | { "type": "Feature", "properties": { "ID": 4 }, "geometry": { "type": "Point", "coordinates": [ 257330.556306812301045, 4368593.322621445171535 ] } }, 11 | { "type": "Feature", "properties": { "ID": 5 }, "geometry": { "type": "Point", "coordinates": [ 259572.892504712508526, 4366263.833810506388545 ] } }, 12 | { "type": "Feature", "properties": { "ID": 6 }, "geometry": { "type": "Point", "coordinates": [ 260094.807748463994358, 4367428.553571276366711 ] } }, 13 | { "type": "Feature", "properties": { "ID": 7 }, "geometry": { "type": "Point", "coordinates": [ 259578.397296969924355, 4369237.928161443211138 ] } }, 14 | { "type": "Feature", "properties": { "ID": 8 }, "geometry": { "type": "Point", "coordinates": [ 258623.660254557529697, 4369319.672552681528032 ] } }, 15 | { "type": "Feature", "properties": { "ID": 9 }, "geometry": { "type": "Point", "coordinates": [ 259735.491290430247318, 4367340.881710163317621 ] } }, 16 | { "type": "Feature", "properties": { "ID": 10 }, "geometry": { "type": "Point", "coordinates": [ 257697.411622470011935, 4368994.436187009327114 ] } }, 17 | { "type": "Feature", "properties": { "ID": 11 }, "geometry": { "type": "Point", "coordinates": [ 257996.218803880474297, 4368439.207917373627424 ] } }, 18 | { "type": "Feature", "properties": { "ID": 12 }, "geometry": { "type": "Point", "coordinates": [ 257599.156646256800741, 4368062.686583452858031 ] } }, 19 | { "type": "Feature", "properties": { "ID": 13 }, "geometry": { "type": "Point", "coordinates": [ 258308.333252404467203, 4369299.130519438534975 ] } }, 20 | { "type": "Feature", "properties": { "ID": 14 }, "geometry": { "type": "Point", "coordinates": [ 259080.394316278165206, 4366406.862520016729832 ] } }, 21 | { "type": "Feature", "properties": { "ID": 15 }, "geometry": { "type": "Point", "coordinates": [ 259152.789523579180241, 4368244.600867603905499 ] } }, 22 | { "type": "Feature", "properties": { "ID": 16 }, "geometry": { "type": "Point", "coordinates": [ 257562.652198452764424, 4368254.556617514230311 ] } }, 23 | { "type": "Feature", "properties": { "ID": 17 }, "geometry": { "type": "Point", "coordinates": [ 259677.723433783103246, 4366579.58681112434715 ] } }, 24 | { "type": "Feature", "properties": { "ID": 18 }, "geometry": { "type": "Point", "coordinates": [ 258222.458088597340975, 4369385.065048774704337 ] } }, 25 | { "type": "Feature", "properties": { "ID": 19 }, "geometry": { "type": "Point", "coordinates": [ 259949.344389050558675, 4368415.55780385620892 ] } }, 26 | { "type": "Feature", "properties": { "ID": 20 }, "geometry": { "type": "Point", "coordinates": [ 259228.049382781697204, 4367121.036382095888257 ] } }, 27 | { "type": "Feature", "properties": { "ID": 21 }, "geometry": { "type": "Point", "coordinates": [ 259244.492506237846101, 4366191.229216520674527 ] } }, 28 | { "type": "Feature", "properties": { "ID": 22 }, "geometry": { "type": "Point", "coordinates": [ 259781.005163653258933, 4366817.544244487769902 ] } }, 29 | { "type": "Feature", "properties": { "ID": 23 }, "geometry": { "type": "Point", "coordinates": [ 260146.613579660916002, 4368846.023989240638912 ] } }, 30 | { "type": "Feature", "properties": { "ID": 24 }, "geometry": { "type": "Point", "coordinates": [ 259555.463881681265775, 4367226.930853052996099 ] } }, 31 | { "type": "Feature", "properties": { "ID": 25 }, "geometry": { "type": "Point", "coordinates": [ 258404.484950041252887, 4367563.285309943370521 ] } }, 32 | { "type": "Feature", "properties": { "ID": 26 }, "geometry": { "type": "Point", "coordinates": [ 257065.438299239671323, 4367526.505432717502117 ] } }, 33 | { "type": "Feature", "properties": { "ID": 27 }, "geometry": { "type": "Point", "coordinates": [ 258149.182815559237497, 4368122.211975787766278 ] } }, 34 | { "type": "Feature", "properties": { "ID": 28 }, "geometry": { "type": "Point", "coordinates": [ 256672.596105954085942, 4367023.978631062433124 ] } }, 35 | { "type": "Feature", "properties": { "ID": 29 }, "geometry": { "type": "Point", "coordinates": [ 256914.319968683703337, 4368452.121861688792706 ] } }, 36 | { "type": "Feature", "properties": { "ID": 30 }, "geometry": { "type": "Point", "coordinates": [ 258588.653580491896719, 4369304.203659546561539 ] } }, 37 | { "type": "Feature", "properties": { "ID": 31 }, "geometry": { "type": "Point", "coordinates": [ 259153.328198893635999, 4368219.731516937725246 ] } }, 38 | { "type": "Feature", "properties": { "ID": 32 }, "geometry": { "type": "Point", "coordinates": [ 259189.947881320520537, 4369216.194828324951231 ] } }, 39 | { "type": "Feature", "properties": { "ID": 33 }, "geometry": { "type": "Point", "coordinates": [ 259233.188176045252476, 4366925.430942944251001 ] } }, 40 | { "type": "Feature", "properties": { "ID": 34 }, "geometry": { "type": "Point", "coordinates": [ 256518.637574030843098, 4367516.658501395955682 ] } }, 41 | { "type": "Feature", "properties": { "ID": 35 }, "geometry": { "type": "Point", "coordinates": [ 260165.236190584721044, 4367541.240417971275747 ] } }, 42 | { "type": "Feature", "properties": { "ID": 36 }, "geometry": { "type": "Point", "coordinates": [ 258513.74904072668869, 4368925.824540394358337 ] } }, 43 | { "type": "Feature", "properties": { "ID": 37 }, "geometry": { "type": "Point", "coordinates": [ 260153.462128713319544, 4368628.122380846180022 ] } }, 44 | { "type": "Feature", "properties": { "ID": 38 }, "geometry": { "type": "Point", "coordinates": [ 259979.349835767963668, 4369154.966496029868722 ] } }, 45 | { "type": "Feature", "properties": { "ID": 39 }, "geometry": { "type": "Point", "coordinates": [ 257065.701165389997186, 4367464.318008036352694 ] } }, 46 | { "type": "Feature", "properties": { "ID": 40 }, "geometry": { "type": "Point", "coordinates": [ 260147.827545514941448, 4368547.410747020505369 ] } }, 47 | { "type": "Feature", "properties": { "ID": 41 }, "geometry": { "type": "Point", "coordinates": [ 259052.29295429866761, 4366510.217707945033908 ] } }, 48 | { "type": "Feature", "properties": { "ID": 42 }, "geometry": { "type": "Point", "coordinates": [ 256887.455658144666813, 4367386.021296887658536 ] } }, 49 | { "type": "Feature", "properties": { "ID": 43 }, "geometry": { "type": "Point", "coordinates": [ 258246.546210634958697, 4368168.48062212113291 ] } }, 50 | { "type": "Feature", "properties": { "ID": 44 }, "geometry": { "type": "Point", "coordinates": [ 258598.780015868105693, 4368215.233078856021166 ] } }, 51 | { "type": "Feature", "properties": { "ID": 45 }, "geometry": { "type": "Point", "coordinates": [ 259231.313788918894716, 4369289.329892785288393 ] } }, 52 | { "type": "Feature", "properties": { "ID": 46 }, "geometry": { "type": "Point", "coordinates": [ 257576.341077287856024, 4367750.393616276793182 ] } }, 53 | { "type": "Feature", "properties": { "ID": 47 }, "geometry": { "type": "Point", "coordinates": [ 258992.739960621634964, 4367516.76409588009119 ] } }, 54 | { "type": "Feature", "properties": { "ID": 48 }, "geometry": { "type": "Point", "coordinates": [ 259704.67851796883042, 4366460.998782421462238 ] } }, 55 | { "type": "Feature", "properties": { "ID": 49 }, "geometry": { "type": "Point", "coordinates": [ 256441.085266035952372, 4366719.059548132121563 ] } }, 56 | { "type": "Feature", "properties": { "ID": 50 }, "geometry": { "type": "Point", "coordinates": [ 257948.149000896257348, 4366957.443306203000247 ] } }, 57 | { "type": "Feature", "properties": { "ID": 51 }, "geometry": { "type": "Point", "coordinates": [ 256455.448828133841744, 4366768.917787346988916 ] } }, 58 | { "type": "Feature", "properties": { "ID": 52 }, "geometry": { "type": "Point", "coordinates": [ 256976.635306953889085, 4368690.457817949354649 ] } }, 59 | { "type": "Feature", "properties": { "ID": 53 }, "geometry": { "type": "Point", "coordinates": [ 258710.684855678642634, 4368770.702041453681886 ] } }, 60 | { "type": "Feature", "properties": { "ID": 54 }, "geometry": { "type": "Point", "coordinates": [ 257922.055744424404111, 4366609.81028286088258 ] } }, 61 | { "type": "Feature", "properties": { "ID": 55 }, "geometry": { "type": "Point", "coordinates": [ 258827.554179900587769, 4367666.895853714086115 ] } }, 62 | { "type": "Feature", "properties": { "ID": 56 }, "geometry": { "type": "Point", "coordinates": [ 256461.321025816199835, 4368227.268166072666645 ] } }, 63 | { "type": "Feature", "properties": { "ID": 57 }, "geometry": { "type": "Point", "coordinates": [ 259701.135867069795495, 4369114.769185935147107 ] } }, 64 | { "type": "Feature", "properties": { "ID": 58 }, "geometry": { "type": "Point", "coordinates": [ 256427.116848229226889, 4368677.779302634298801 ] } }, 65 | { "type": "Feature", "properties": { "ID": 59 }, "geometry": { "type": "Point", "coordinates": [ 258719.164605976227904, 4367248.217307215556502 ] } }, 66 | { "type": "Feature", "properties": { "ID": 60 }, "geometry": { "type": "Point", "coordinates": [ 257042.756496701884316, 4368899.217331963591278 ] } }, 67 | { "type": "Feature", "properties": { "ID": 61 }, "geometry": { "type": "Point", "coordinates": [ 259699.634637012553867, 4366636.636728246696293 ] } }, 68 | { "type": "Feature", "properties": { "ID": 62 }, "geometry": { "type": "Point", "coordinates": [ 259911.356095109746093, 4368838.904539418406785 ] } }, 69 | { "type": "Feature", "properties": { "ID": 63 }, "geometry": { "type": "Point", "coordinates": [ 259176.439046767394757, 4368177.549343947321177 ] } }, 70 | { "type": "Feature", "properties": { "ID": 64 }, "geometry": { "type": "Point", "coordinates": [ 257340.295964584947797, 4369191.098772367462516 ] } }, 71 | { "type": "Feature", "properties": { "ID": 65 }, "geometry": { "type": "Point", "coordinates": [ 257442.145556407951517, 4368496.149814907461405 ] } }, 72 | { "type": "Feature", "properties": { "ID": 66 }, "geometry": { "type": "Point", "coordinates": [ 258828.561352491000434, 4368787.285905389115214 ] } }, 73 | { "type": "Feature", "properties": { "ID": 67 }, "geometry": { "type": "Point", "coordinates": [ 256990.046184232021915, 4366419.007601706311107 ] } }, 74 | { "type": "Feature", "properties": { "ID": 68 }, "geometry": { "type": "Point", "coordinates": [ 257293.582920640095836, 4366844.409745059907436 ] } }, 75 | { "type": "Feature", "properties": { "ID": 69 }, "geometry": { "type": "Point", "coordinates": [ 259351.676484935043845, 4368637.316409929655492 ] } }, 76 | { "type": "Feature", "properties": { "ID": 70 }, "geometry": { "type": "Point", "coordinates": [ 257360.512931298668263, 4369229.916526658460498 ] } }, 77 | { "type": "Feature", "properties": { "ID": 71 }, "geometry": { "type": "Point", "coordinates": [ 259904.805409144755686, 4368019.258245759643614 ] } }, 78 | { "type": "Feature", "properties": { "ID": 72 }, "geometry": { "type": "Point", "coordinates": [ 260050.840477904741419, 4369030.695645220577717 ] } }, 79 | { "type": "Feature", "properties": { "ID": 73 }, "geometry": { "type": "Point", "coordinates": [ 256703.067536346963607, 4366396.052628544159234 ] } }, 80 | { "type": "Feature", "properties": { "ID": 74 }, "geometry": { "type": "Point", "coordinates": [ 256717.34813010256039, 4368955.435298060998321 ] } }, 81 | { "type": "Feature", "properties": { "ID": 75 }, "geometry": { "type": "Point", "coordinates": [ 260037.17948546691332, 4369014.056687485426664 ] } }, 82 | { "type": "Feature", "properties": { "ID": 76 }, "geometry": { "type": "Point", "coordinates": [ 259875.911965812789276, 4368164.076143584214151 ] } }, 83 | { "type": "Feature", "properties": { "ID": 77 }, "geometry": { "type": "Point", "coordinates": [ 259559.861625146673759, 4367878.174662289209664 ] } }, 84 | { "type": "Feature", "properties": { "ID": 78 }, "geometry": { "type": "Point", "coordinates": [ 259049.59412794635864, 4368875.119923547841609 ] } }, 85 | { "type": "Feature", "properties": { "ID": 79 }, "geometry": { "type": "Point", "coordinates": [ 260013.626788953144569, 4367948.249511840753257 ] } }, 86 | { "type": "Feature", "properties": { "ID": 80 }, "geometry": { "type": "Point", "coordinates": [ 259794.496422457305016, 4367682.355783678591251 ] } }, 87 | { "type": "Feature", "properties": { "ID": 81 }, "geometry": { "type": "Point", "coordinates": [ 259795.746768842305755, 4367542.340710313990712 ] } }, 88 | { "type": "Feature", "properties": { "ID": 82 }, "geometry": { "type": "Point", "coordinates": [ 256600.351418374018976, 4366487.852100069634616 ] } }, 89 | { "type": "Feature", "properties": { "ID": 83 }, "geometry": { "type": "Point", "coordinates": [ 257584.526516528159846, 4366658.624278666451573 ] } }, 90 | { "type": "Feature", "properties": { "ID": 84 }, "geometry": { "type": "Point", "coordinates": [ 258407.144532597711077, 4369241.488155446015298 ] } }, 91 | { "type": "Feature", "properties": { "ID": 85 }, "geometry": { "type": "Point", "coordinates": [ 258655.95327439007815, 4369083.220562373287976 ] } }, 92 | { "type": "Feature", "properties": { "ID": 86 }, "geometry": { "type": "Point", "coordinates": [ 257173.227913249022095, 4366292.919162608683109 ] } }, 93 | { "type": "Feature", "properties": { "ID": 87 }, "geometry": { "type": "Point", "coordinates": [ 258023.353815670881886, 4366788.055239440873265 ] } }, 94 | { "type": "Feature", "properties": { "ID": 88 }, "geometry": { "type": "Point", "coordinates": [ 259887.923449679306941, 4366409.897931963205338 ] } }, 95 | { "type": "Feature", "properties": { "ID": 89 }, "geometry": { "type": "Point", "coordinates": [ 257114.253776938770898, 4366449.637442946434021 ] } }, 96 | { "type": "Feature", "properties": { "ID": 90 }, "geometry": { "type": "Point", "coordinates": [ 257226.960599530080799, 4368291.820152725093067 ] } }, 97 | { "type": "Feature", "properties": { "ID": 91 }, "geometry": { "type": "Point", "coordinates": [ 258147.473716928274371, 4369318.57885108422488 ] } }, 98 | { "type": "Feature", "properties": { "ID": 92 }, "geometry": { "type": "Point", "coordinates": [ 258801.152255163848167, 4368008.586766583845019 ] } }, 99 | { "type": "Feature", "properties": { "ID": 93 }, "geometry": { "type": "Point", "coordinates": [ 257014.825970180419972, 4368867.873645421117544 ] } }, 100 | { "type": "Feature", "properties": { "ID": 94 }, "geometry": { "type": "Point", "coordinates": [ 257728.362570064491592, 4366309.764949266798794 ] } }, 101 | { "type": "Feature", "properties": { "ID": 95 }, "geometry": { "type": "Point", "coordinates": [ 257248.177467843925115, 4368784.922595512121916 ] } }, 102 | { "type": "Feature", "properties": { "ID": 96 }, "geometry": { "type": "Point", "coordinates": [ 259631.952965407283045, 4367405.784822245128453 ] } }, 103 | { "type": "Feature", "properties": { "ID": 97 }, "geometry": { "type": "Point", "coordinates": [ 257266.991436359356157, 4366681.87734058406204 ] } }, 104 | { "type": "Feature", "properties": { "ID": 98 }, "geometry": { "type": "Point", "coordinates": [ 256853.229541760316351, 4367624.895734518766403 ] } }, 105 | { "type": "Feature", "properties": { "ID": 99 }, "geometry": { "type": "Point", "coordinates": [ 259708.947856223821873, 4366164.786713152192533 ] } } 106 | ] 107 | } 108 | -------------------------------------------------------------------------------- /sample-data/polygons.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::26918" } }, 4 | 5 | "features": [ 6 | { "type": "Feature", "properties": { }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 256864.273509610560723, 4369446.92893316783011 ], [ 256582.803696377231972, 4369318.257018529810011 ], [ 256647.13965368657955, 4369109.165157290175557 ], [ 257161.827312172652455, 4368940.283269408158958 ], [ 257306.583216122438898, 4369334.341007919982076 ], [ 256896.44148826651508, 4369149.375130629166961 ], [ 256816.021541627909755, 4369294.131034560501575 ], [ 256816.021541627909755, 4369294.131034560501575 ], [ 256864.273509610560723, 4369446.92893316783011 ] ] ] } }, 7 | { "type": "Feature", "properties": { }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 256568.686714106646832, 4367849.569086695089936 ], [ 256407.044597075175261, 4367422.372063129208982 ], [ 257053.613065199228004, 4367203.000618660822511 ], [ 257111.342392708989792, 4367572.468314710073173 ], [ 256568.686714106646832, 4367849.569086695089936 ] ] ] } }, 8 | { "type": "Feature", "properties": { }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 259069.296735104784602, 4369426.075128388591111 ], [ 258894.895526775741018, 4369347.013247269205749 ], [ 258760.025259000714868, 4369077.27271172683686 ], [ 258973.957407884736313, 4368712.19284899905324 ], [ 259018.1390473281499, 4368768.001235664822161 ], [ 258878.618080664600711, 4369137.731797285377979 ], [ 259069.296735104784602, 4369426.075128388591111 ] ] ] } }, 9 | { "type": "Feature", "properties": { }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 258222.869537346123252, 4367389.069015161134303 ], [ 257515.963306249934249, 4366579.847408504225314 ], [ 258222.869537347607547, 4366133.380315273068845 ], [ 258222.869537346123252, 4367389.069015161134303 ] ] ] } }, 10 | { "type": "Feature", "properties": { }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 259832.011352866480593, 4367965.755677470937371 ], [ 259450.65404398686951, 4367547.192777475342155 ], [ 259897.121137309324695, 4366784.478159802034497 ], [ 260185.464468414720614, 4367640.206755305640399 ], [ 259832.011352866480593, 4367965.755677470937371 ] ] ] } }, 11 | { "type": "Feature", "properties": { }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 258176.3625484565855, 4368635.4563172692433 ], [ 259125.105121770204278, 4367984.358472964726388 ], [ 259087.899530660710298, 4367909.947290746495128 ], [ 258966.981359552970389, 4367584.398368541151285 ], [ 258725.145017336006276, 4367575.096970743499696 ], [ 258371.691901786572998, 4367909.947290684096515 ], [ 258157.759752902551554, 4368235.496212858706713 ], [ 258176.3625484565855, 4368635.4563172692433 ] ] ] } } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /sample-data/small-aoi-polygon-line.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::26918" } }, 4 | 5 | "features": [ 6 | { "type": "Feature", "properties": { }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 256851.544156655087136, 4367954.834547673352063 ], [ 258393.566504797519883, 4368037.17773833218962 ], [ 258685.503635721688624, 4366637.38058122433722 ], [ 257315.649196166690672, 4366570.011919111013412 ], [ 256851.544156655087136, 4367954.834547673352063 ] ] ] } } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal: 1 3 | 4 | [tool:pytest] 5 | testpaths: tests 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | """Setup script for ``gj1ascii``.""" 5 | 6 | 7 | import itertools as it 8 | import os 9 | 10 | from setuptools import find_packages 11 | from setuptools import setup 12 | 13 | 14 | with open('README.rst') as f: 15 | readme = f.read().strip() 16 | 17 | 18 | def parse_dunder_line(string): 19 | 20 | """Take a line like: 21 | 22 | "__version__ = '0.0.8'" 23 | 24 | and turn it into a tuple: 25 | 26 | ('__version__', '0.0.8') 27 | 28 | Not very fault tolerant. 29 | """ 30 | 31 | # Split the line and remove outside quotes 32 | variable, value = (s.strip() for s in string.split('=')[:2]) 33 | value = value[1:-1].strip() 34 | return variable, value 35 | 36 | 37 | with open(os.path.join('gj2ascii', '__init__.py')) as f: 38 | dunders = dict(map( 39 | parse_dunder_line, filter(lambda l: l.rstrip().startswith('__'), f))) 40 | version = dunders['__version__'] 41 | author = dunders['__author__'] 42 | email = dunders['__email__'] 43 | source = dunders['__source__'] 44 | 45 | 46 | extras_require = { 47 | 'test': ['pytest>=3', 'pytest-cov', 'coveralls'], 48 | 'emoji': ['emoji>=0.3.4'] 49 | } 50 | extras_require['all'] = list(it.chain.from_iterable(extras_require.values())) 51 | 52 | 53 | setup( 54 | name='gj2ascii', 55 | author=author, 56 | author_email=email, 57 | classifiers=[ 58 | 'Development Status :: 4 - Beta', 59 | 'Intended Audience :: Developers', 60 | 'License :: OSI Approved :: BSD License', 61 | 'Intended Audience :: Information Technology', 62 | 'Programming Language :: Python :: 2.7', 63 | 'Programming Language :: Python :: 3.3', 64 | 'Programming Language :: Python :: 3.4', 65 | 'Programming Language :: Python :: 3.5', 66 | 'Programming Language :: Python :: 3.6', 67 | 'Topic :: Multimedia :: Graphics :: Presentation', 68 | 'Topic :: Scientific/Engineering :: GIS', 69 | 'Topic :: Utilities' 70 | ], 71 | description="Render geospatial vectors as text.", 72 | entry_points=""" 73 | [console_scripts] 74 | gj2ascii=gj2ascii.cli:main 75 | """, 76 | include_package_data=True, 77 | install_requires=[ 78 | 'click>=3.0', 79 | 'fiona>=1.2', 80 | 'numpy>=1.8', 81 | 'rasterio>=0.18', 82 | 'shapely' 83 | ], 84 | extras_require=extras_require, 85 | keywords='geojson ascii text emoji gis vector render', 86 | license="New BSD", 87 | long_description=readme, 88 | packages=find_packages(exclude=['tests']), 89 | url=source, 90 | version=version, 91 | zip_safe=True 92 | ) 93 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """``pytest`` fixtures.""" 2 | 3 | 4 | import os 5 | 6 | from click.testing import CliRunner 7 | import pytest 8 | 9 | 10 | @pytest.fixture(scope='function') 11 | def runner(): 12 | return CliRunner() 13 | 14 | 15 | @pytest.fixture(scope='module') 16 | def expected_polygon_40_wide(): 17 | return os.linesep.join([ 18 | '. + . . . . . . . . . . . + . . . . . .', 19 | '. + + + . . . . . . . . . . . . . . . .', 20 | '. . . + . . . . . . . . . . . . . . . .', 21 | '. . . . . . . . . . . . . + . . . . . .', 22 | '. . . . . . . . . + . . . . . . . . . .', 23 | '. . . . . . . . . + + . . . . . . . . .', 24 | '. . . . . . . . . + + + + . . . . . . .', 25 | '. . . . . . . . . . + + + + . . . . . .', 26 | '. . . . . . . . . . . + + + . . . . + .', 27 | '+ + + . . . . . . . . . + + . . . + + +', 28 | '+ + + + . . . . . . . . . . . . + + + +', 29 | '. . + . . . . . . + . . . . . . . + + .', 30 | '. . . . . . . . + + . . . . . . . . + .', 31 | '. . . . . . . + + + . . . . . . . . + .', 32 | '. . . . . . + + + + . . . . . . . . . .', 33 | '. . . . . . + + + + . . . . . . . . . .', 34 | '. . . . . . . . + + . . . . . . . . . .' 35 | ]) 36 | 37 | 38 | @pytest.fixture(scope='module') 39 | def expected_line_40_wide(): 40 | return os.linesep.join([ 41 | '. . . . . . . . + + + + + + + + + + . .', 42 | '+ + + + + + + + + . . . . . . . . . . .', 43 | '+ + . . . . . . . . . . . . . . . . . .', 44 | '. + + . . . . . . . . . . . . . . . . .', 45 | '. . + + . . . . . . . . . . . . . . . .', 46 | '. . . + + + . . . . . . . . . . . . . .', 47 | '. . . . . + + . . . . . . . . . . . . .', 48 | '. . . . . . + + . . . . . . . . . . . .', 49 | '. . . . . . . + + . . . . . . . . . . .', 50 | '. . . . . . . . + + + . . . . . . . . .', 51 | '. . . . . . . . . . + + . . . . . . . .', 52 | '. . . . . . . . . . + . . . . . . . . .', 53 | '. . . . . . . . . + + . . . . . . . . .', 54 | '. . . . . . . . + + . . . . . . . . . .', 55 | '. . . . . . . . + . . . . . . . . . . .', 56 | '. . . . . . . + + . . . . . . . . . . .', 57 | '. . . . . . + + . . . . . . . . . . . .', 58 | '. . . . . + + . . . . . . . . . . . . .', 59 | '. . . . . + . . . . . . . . . . . . . .', 60 | '. . . . + + . . . . . . . . . . . . . .', 61 | '. . . + + . . . . . . . . . . . . . . .', 62 | '. . . + . . . . . . . . . . . . . . . .', 63 | '. . + + . . . . . . . . . . . . . . . .', 64 | '. + + . . . . . . . . . . . . . . . . .', 65 | '. + . . . . . . . . + . . . . . . . . .', 66 | '. + + . . . . . . . + + . . . . . . . .', 67 | '. . + . . . . . . . + + + . . . . . . .', 68 | '. . + + . . . . . . + . + + . . . . . .', 69 | '. . . + + . . . . . + . . + + . . . . .', 70 | '. . . . + + . . . . + . . . + + . . . .', 71 | '. . . . . + + . . . + . . . . + + . . .', 72 | '. . . . . . + + . . + . . . . . + + . .', 73 | '. . . . . . . + + . + . . . . . . + + .', 74 | '. . . . . . . . + + + . . . . . . . + +', 75 | '', 76 | '+ . . . . . . . . . . . . . . . . . . .', 77 | '+ . . . . . . . . . . . . . . . . . . .', 78 | '+ + . . . . . . . . . . . . . . . . . .', 79 | '. + . . . . . . . . . . . . . . . . . .', 80 | '. + . . . . . . . . . . . . . . . . . .', 81 | '. + + . . . . . . . . . . . . . . . . .', 82 | '. . + . . . . . . . . . . . . . . . . .', 83 | '. . + . . . . . . . . . . . . . . . . .', 84 | '. . + + . . . . . . . . . . . . . . . .', 85 | '. . . + . . . . . . . . . . . . . . . .', 86 | '. . . + . . . . . . . . . . . . . . . .', 87 | '. . . + + . . . . . . . . . . . . . . .', 88 | '. . . . + . . . . . . . . . . . . . . .', 89 | '. . . . + + . . . . . . . . . . . . . .', 90 | '. . . . . + . . . . . . . . . . . . . .', 91 | '. . . . . + . . . . . . . . . . . . . .', 92 | '. . . . . + + . . . . . . . . . . . . .', 93 | '. . . . . . + . . . . . . . . . . . . .', 94 | '. . . . . . + . . . . . . . . . . . . .', 95 | '. . . . . . + + . . . . . . . . . . . .', 96 | '. . . . . . . + . . . . . . . . . . . .', 97 | '. . . . . . . + . . . . . . . . . . . .', 98 | '. . . . . . . + + . . . . . . . . . . .', 99 | '. . . . . . . . + . . . . . . . . . . .', 100 | '. . . . . . . . + + . . . . . . . . . .', 101 | '. . . . . . . . . + . . . . . . . . . .', 102 | '. . . . . . . . . + . . . . . . . . . .', 103 | '. . . . . . . . . + + . . . . . . . . .', 104 | '. . . . . . . . . . + . . . . . . . . .', 105 | '. . . . . . . . . . + . . . . . . . . .', 106 | '. . . . . . . . . . + + . . . . . . . .', 107 | '. . . . . . . . . . . + . . . . . . . .', 108 | '. . . . . . . . . . . + . . . . . . . .', 109 | '. . . . . . . . . . . + + . . . . . . .', 110 | '. . . . . . . . . . . . + . . . . . . .', 111 | '. . . . . . . . . . . . + . . . . . . .', 112 | '. . . . . . . . . . . . + + . . . . . .', 113 | '. . . . . . . . . . . . . + . . . . . .', 114 | '. . . . . . . . . . . . . + + . . . . .', 115 | '. . . . . . . . . . . . . . + . . . . .', 116 | '. . . . . . . . . . . . . . + . . . . .', 117 | '. . . . . . . . . . . . . . + + . . . .', 118 | '. . . . . . . . . . . . . . . + . . . .', 119 | '. . . . . . . . . . . . . . . + . . . .', 120 | '. . . . . . . . . . . . . . . + + . . .', 121 | '. . . . . . . . . . . . . . . . + . . .', 122 | '. . . . . . . . . . . . . . . . + . . .', 123 | '. . . . . . . . . . . . . . . . + + . .', 124 | '. . . . . . . . . . . . . . . . . + . .', 125 | '. . . . . . . . . . . . . . . . . + + .', 126 | '. . . . . . . . . . . . . . . . . . + .', 127 | '. . . . . . . . . . . . . . . . . . + .', 128 | '. . . . . . . . . . . . . . . . . . + +', 129 | '. . . . . . . . . . . . . . . . . . . +', 130 | '. . . . . . . . . . . . . . . . . . . +', 131 | '', 132 | '+ + + + + + + + + + + + + + + + + + + +', 133 | '']) 134 | 135 | 136 | @pytest.fixture(scope='module') 137 | def expected_all_properties_output(): 138 | return os.linesep.join([ 139 | '+----------+----------------+', 140 | '| STATEFP | 54 |', 141 | '| COUNTYFP | 001 |', 142 | '| COUNTYNS | 01696996 |', 143 | '| GEOID | 54001 |', 144 | '| NAME | Barbour |', 145 | '| NAMELSAD | Barbour County |', 146 | '| LSAD | 06 |', 147 | '| CLASSFP | H1 |', 148 | '| MTFCC | G4020 |', 149 | '| CSAFP | None |', 150 | '| CBSAFP | None |', 151 | '| METDIVFP | None |', 152 | '| FUNCSTAT | A |', 153 | '| ALAND | 883338808 |', 154 | '| AWATER | 4639183 |', 155 | '| INTPTLAT | +39.1397248 |', 156 | '| INTPTLON | -079.9969466 |', 157 | '+----------+----------------+', 158 | ' ', 159 | ' ', 160 | ' + + + + + +', 161 | ' + + + + + +', 162 | '+ + + + + +', 163 | '+ + + + +', 164 | '', 165 | '']) 166 | 167 | 168 | @pytest.fixture(scope='function') 169 | def expected_two_properties_output(): 170 | return os.linesep.join([ 171 | '+-------+-----------+', 172 | '| NAME | Barbour |', 173 | '| ALAND | 883338808 |', 174 | '+-------+-----------+', 175 | '* * * * * * * * * *', 176 | '* * * * * * * * * *', 177 | '* * * + + + * + + +', 178 | '* * + + + + + + * *', 179 | '+ + + + + + * * * *', 180 | '+ + + + + * * * * *', 181 | '', 182 | '' 183 | ]) 184 | 185 | 186 | @pytest.fixture(scope='module') 187 | def poly_file(): 188 | return os.path.join('sample-data', 'polygons.geojson') 189 | 190 | 191 | @pytest.fixture(scope='module') 192 | def line_file(): 193 | return os.path.join('sample-data', 'lines.geojson') 194 | 195 | 196 | @pytest.fixture(scope='module') 197 | def point_file(): 198 | return os.path.join('sample-data', 'points.geojson') 199 | 200 | 201 | @pytest.fixture(scope='module') 202 | def single_feature_wv_file(): 203 | return os.path.join('sample-data', 'single-feature-WV.geojson') 204 | 205 | 206 | @pytest.fixture(scope='module') 207 | def multilayer_file(): 208 | return os.path.join('sample-data', 'multilayer-polygon-line') 209 | 210 | 211 | @pytest.fixture(scope='module') 212 | def small_aoi_poly_line_file(): 213 | return os.path.join('sample-data', 'small-aoi-polygon-line.geojson') 214 | 215 | 216 | @pytest.fixture(scope='module') 217 | def compare_ascii(): 218 | def _compare_ascii(text1, text2): 219 | a = [l.rstrip() for l in text1.strip().splitlines()] 220 | b = [l.rstrip() for l in text2.strip().splitlines()] 221 | if a == b: 222 | return True 223 | else: 224 | raise Exception("{} != {}".format(repr(text1), repr(text2))) 225 | return _compare_ascii 226 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unittests for gj2ascii CLI 3 | """ 4 | 5 | 6 | from __future__ import division 7 | 8 | import os 9 | import tempfile 10 | import unittest 11 | 12 | import click 13 | import emoji 14 | import fiona as fio 15 | import pytest 16 | 17 | import gj2ascii 18 | from gj2ascii import cli 19 | 20 | 21 | def test_complex(runner, expected_line_40_wide, line_file, compare_ascii): 22 | result = runner.invoke(cli.main, [ 23 | line_file, 24 | '--width', '40', 25 | '--char', '+', 26 | '--fill', '.', 27 | '--no-prompt', 28 | '--all-touched', 29 | '--iterate', 30 | '--crs', 'EPSG:26918' 31 | ]) 32 | assert result.exit_code == 0 33 | assert compare_ascii(result.output, expected_line_40_wide) 34 | 35 | 36 | def test_bad_fill_value(runner, poly_file): 37 | result = runner.invoke(cli.main, ['-c toolong', poly_file]) 38 | assert result.exit_code != 0 39 | assert result.output.startswith('Usage:') 40 | assert 'Error:' in result.output 41 | assert 'must be a single character' in result.output 42 | 43 | 44 | def test_bad_rasterize_value(runner, poly_file): 45 | result = runner.invoke(cli.main, ['-f toolong', poly_file]) 46 | assert result.exit_code != 0 47 | assert result.output.startswith('Usage:') 48 | assert 'Error:' in result.output 49 | assert 'must be a single character' in result.output 50 | 51 | 52 | def test_render_one_layer_too_many_args(runner, poly_file): 53 | result = runner.invoke(cli.main, [ 54 | poly_file, 55 | '--char', '-', 56 | '--char', '8' 57 | ]) 58 | assert result.exit_code != 0 59 | assert result.output.startswith('Error:') 60 | assert 'number' in result.output 61 | assert '--char' in result.output 62 | 63 | 64 | def test_different_width(runner, poly_file): 65 | fill = '+' 66 | value = '.' 67 | width = 62 68 | result = runner.invoke(cli.main, [ 69 | '--width', width, 70 | poly_file, 71 | '--fill', fill, 72 | '--char', value, 73 | '--no-prompt' 74 | ]) 75 | assert result.exit_code == 0 76 | for line in result.output.rstrip(os.linesep).splitlines(): 77 | if line.startswith((fill, value)): 78 | assert len(line.rstrip(os.linesep).split()) == width / 2 79 | 80 | 81 | def test_iterate_wrong_arg_count(runner, poly_file): 82 | result = runner.invoke(cli.main, [ 83 | poly_file, 84 | '--iterate', 85 | '--char', '1', 86 | '--char', '2' 87 | ]) 88 | assert result.exit_code != 0 89 | assert result.output.startswith('Error:') 90 | assert 'arg' in result.output 91 | assert 'layer' in result.output 92 | 93 | 94 | def test_bbox(runner, poly_file, small_aoi_poly_line_file, compare_ascii): 95 | expected = os.linesep.join([ 96 | ' + + + +', 97 | ' + + +', 98 | ' + +', 99 | ' +', 100 | '+ +', 101 | '+ + +', 102 | '+ + +', 103 | '+ +', 104 | '+ + +', 105 | ' + +', 106 | ' + + +', 107 | ' + + + +', 108 | ' + + + + +', 109 | ' + + + + + +', 110 | ' + + + + + + +', 111 | '' 112 | ]) 113 | with fio.open(small_aoi_poly_line_file) as src: 114 | cmd = [ 115 | poly_file, 116 | '--width', '40', 117 | '--char', '+', 118 | '--bbox', 119 | ] + list(map(str, src.bounds)) 120 | result = runner.invoke(cli.main, cmd) 121 | assert result.exit_code == 0 122 | assert compare_ascii(result.output.strip(), expected.strip()) 123 | 124 | 125 | def test_exceed_auto_generate_colormap_limit(runner, poly_file): 126 | infiles = [poly_file for i in range(len(gj2ascii.ANSI_COLORMAP.keys()) + 2)] 127 | result = runner.invoke(cli.main, infiles) 128 | assert result.exit_code != 0 129 | assert result.output.startswith('Error:') 130 | assert 'auto' in result.output 131 | assert 'generate' in result.output 132 | assert '--char' in result.output 133 | 134 | 135 | def test_default_char_map(runner, poly_file, compare_ascii): 136 | with fio.open(poly_file) as src: 137 | expected = gj2ascii.render(src) 138 | result = runner.invoke(cli.main, [ 139 | poly_file 140 | ]) 141 | assert result.exit_code == 0 142 | assert compare_ascii(result.output.strip(), expected.strip()) 143 | 144 | 145 | def test_same_char_twice(runner, poly_file, line_file, compare_ascii): 146 | width = 40 147 | fill = '.' 148 | char = '+' 149 | with fio.open(poly_file) as poly, fio.open(line_file) as line: 150 | coords = list(poly.bounds) + list(line.bounds) 151 | bbox = (min(coords[0::4]), min(coords[1::4]), max(coords[2::4]), max(coords[3::4])) 152 | expected = gj2ascii.render_multiple( 153 | [(poly, char), (line, char)], width=width, fill=fill, bbox=bbox) 154 | result = runner.invoke(cli.main, [ 155 | poly_file, line_file, 156 | '--width', width, 157 | '--char', char, 158 | '--char', char, 159 | '--fill', fill 160 | ]) 161 | assert result.exit_code == 0 162 | assert compare_ascii(expected, result.output) 163 | 164 | 165 | def test_iterate_bad_property(runner, single_feature_wv_file): 166 | result = runner.invoke(cli.main, [ 167 | single_feature_wv_file, 168 | '--iterate', 169 | '--properties', 'bad-prop' 170 | ]) 171 | assert result.exit_code != 0 172 | assert isinstance(result.exception, KeyError) 173 | 174 | 175 | def test_styled_write_to_file(runner, single_feature_wv_file, compare_ascii): 176 | with fio.open(single_feature_wv_file) as src: 177 | expected = gj2ascii.render(src, width=20, char='1', fill='0') 178 | with tempfile.NamedTemporaryFile('r+') as f: 179 | result = runner.invoke(cli.main, [ 180 | single_feature_wv_file, 181 | '--width', '20', 182 | '--properties', 'NAME,ALAND', 183 | '--char', '1=red', 184 | '--fill', '0=blue', 185 | '--outfile', f.name 186 | ]) 187 | f.seek(0) 188 | assert result.exit_code == 0 189 | assert result.output == '' 190 | assert compare_ascii(f.read().strip(), expected.strip()) 191 | 192 | 193 | def test_stack_too_many_args(runner, multilayer_file): 194 | result = runner.invoke(cli.main, [ 195 | multilayer_file + ',polygons,lines', 196 | '--char', '+', 197 | '--char', '8', 198 | '--char', '0' # 2 layers but 3 values 199 | ]) 200 | assert result.exit_code != 0 201 | assert result.output.startswith('Error:') 202 | assert '--char' in result.output 203 | assert 'number' in result.output 204 | assert 'equal' in result.output 205 | 206 | 207 | def test_iterate_too_many_layers(runner, multilayer_file): 208 | result = runner.invoke(cli.main, [ 209 | multilayer_file, 210 | '--iterate', '--no-prompt' 211 | ]) 212 | assert result.exit_code != 0 213 | assert result.output.startswith('Error:') 214 | assert 'single layer' in result.output 215 | 216 | 217 | def test_multilayer_compute_colormap(runner, multilayer_file, compare_ascii): 218 | coords = [] 219 | for layer in ('polygons', 'lines'): 220 | with fio.open(multilayer_file, layer=layer) as src: 221 | coords += list(src.bounds) 222 | bbox = min(coords[0::4]), min(coords[1::4]), max(coords[2::4]), max(coords[3::4]) 223 | 224 | rendered_layers = [] 225 | for layer, char in zip(('polygons', 'lines'), ('0', '1')): 226 | with fio.open(multilayer_file, layer=layer) as src: 227 | rendered_layers.append( 228 | gj2ascii.render(src, width=20, fill=' ', char=char, bbox=bbox)) 229 | expected = gj2ascii.stack(rendered_layers) 230 | 231 | # Explicitly define since layers are not consistently listed in order 232 | result = runner.invoke(cli.main, [ 233 | multilayer_file + ',polygons,lines', 234 | '--width', '20' 235 | ]) 236 | assert result.exit_code == 0 237 | assert compare_ascii(expected.strip(), result.output.strip()) 238 | 239 | 240 | def test_stack_layers(runner, multilayer_file, compare_ascii): 241 | expected = os.linesep.join([ 242 | '. + . . . . . . . . . . . + . . . . . .', 243 | '. + + + . . . . . . . . . . . . . . . .', 244 | '. . 8 8 8 8 8 8 8 . . . . 8 . . . . . .', 245 | '. . . 8 . . . . . . . . . 8 . . . . . .', 246 | '. . . . 8 . . . . + . . . . 8 . . . . .', 247 | '. . . . . 8 . . . + + . . . 8 . . . . .', 248 | '. . . . . . 8 . . + + + + . 8 . . . . .', 249 | '. . . . . 8 . . . . + + + + . 8 . . . .', 250 | '. . . . 8 . . . . . . 8 8 8 . 8 . . + .', 251 | '+ + + . 8 . . . 8 8 8 . + + . . 8 + + +', 252 | '+ + + 8 . . . . . . . . . . . . 8 + + +', 253 | '. . 8 . . . 8 . . + . . . . . . 8 + + .', 254 | '. . . 8 . . 8 8 + + . . . . . . . 8 + .', 255 | '. . . . 8 . 8 + 8 + . . . . . . . 8 + .', 256 | '. . . . 8 8 + + 8 + . . . . . . . . . .', 257 | '. . . . . 8 + + + 8 . . . . . . . . . .', 258 | '. . . . . . . . + + . . . . . . . . . .' 259 | ]) 260 | result = runner.invoke(cli.main, [ 261 | multilayer_file + ',polygons,lines', 262 | '--char', '+', 263 | '--char', '8', 264 | '--fill', '.', 265 | '--width', '40' 266 | ]) 267 | assert result.exit_code == 0 268 | assert compare_ascii(result.output.strip(), expected) 269 | 270 | 271 | def test_write_to_file(runner, single_feature_wv_file, compare_ascii): 272 | expected = os.linesep.join([ 273 | '+-------+-----------+', 274 | '| NAME | Barbour |', 275 | '| ALAND | 883338808 |', 276 | '+-------+-----------+', 277 | '* * * * * * * * * *', 278 | '* * * * * * * * * *', 279 | '* * * + + + * + + +', 280 | '* * + + + + + + * *', 281 | '+ + + + + + * * * *', 282 | '+ + + + + * * * * *' 283 | ]) 284 | with tempfile.NamedTemporaryFile('r+') as f: 285 | result = runner.invoke(cli.main, [ 286 | single_feature_wv_file, 287 | '--width', '20', 288 | '--properties', 'NAME,ALAND', 289 | '--iterate', 290 | '--fill', '*', 291 | '--outfile', f.name 292 | # --no-prompt should automatically happen in this case 293 | ]) 294 | f.seek(0) 295 | assert result.exit_code == 0 296 | assert result.output == '' 297 | assert compare_ascii( 298 | f.read().strip(), expected) 299 | 300 | 301 | @pytest.mark.xfail( 302 | os.environ.get('TRAVIS', '').lower() == 'true', 303 | reason='Failing on Travis for an unknown reason.') 304 | def test_paginate_with_all_properties( 305 | runner, expected_all_properties_output, single_feature_wv_file, 306 | compare_ascii): 307 | result = runner.invoke(cli.main, [ 308 | single_feature_wv_file, 309 | '--width', '20', 310 | '--properties', '%all', 311 | '--iterate', '--no-prompt' 312 | ]) 313 | assert result.exit_code == 0 314 | assert compare_ascii(result.output, expected_all_properties_output) 315 | 316 | 317 | @pytest.mark.xfail( 318 | os.environ.get('TRAVIS', '').lower() == 'true', 319 | reason='Failing on Travis for an unknown reason.') 320 | def test_paginate_with_two_properties( 321 | runner, expected_two_properties_output, single_feature_wv_file, 322 | compare_ascii): 323 | result = runner.invoke(cli.main, [ 324 | single_feature_wv_file, 325 | '--width', '20', 326 | '--fill', '*', 327 | '--properties', 'NAME,ALAND', 328 | '--iterate', '--no-prompt' 329 | ]) 330 | assert result.exit_code == 0 331 | assert compare_ascii(result.output, expected_two_properties_output) 332 | 333 | 334 | def test_simple(runner, expected_polygon_40_wide, poly_file, compare_ascii): 335 | result = runner.invoke(cli.main, [ 336 | poly_file, 337 | '--width', '40', 338 | '--char', '+', 339 | '--fill', '.', 340 | ]) 341 | assert result.exit_code == 0 342 | assert compare_ascii(result.output.strip(), expected_polygon_40_wide) 343 | 344 | 345 | def test_cb_char_and_fill(): 346 | testvals = { 347 | 'a': [('a', None)], 348 | ('a', 'b'): [('a', None), ('b', None)], 349 | 'black': [(gj2ascii.DEFAULT_COLOR_CHAR['black'], 'black')], 350 | ('black', 'blue'): [ 351 | (gj2ascii.DEFAULT_COLOR_CHAR['black'], 'black'), 352 | (gj2ascii.DEFAULT_COLOR_CHAR['blue'], 'blue') 353 | ], 354 | ('+=red', '==yellow'): [('+', 'red'), ('=', 'yellow')], 355 | None: [] 356 | } 357 | 358 | # The callback can return a list of tuples or a list of lists. Force test to compare 359 | # list to list. 360 | for inval, expected in testvals.items(): 361 | expected = [list(i) for i in expected] 362 | actual = [list(i) for i in cli._cb_char_and_fill(None, None, inval)] 363 | assert expected == actual 364 | with pytest.raises(click.BadParameter): 365 | cli._cb_char_and_fill(None, None, 'bad-color') 366 | with pytest.raises(click.BadParameter): 367 | cli._cb_char_and_fill(None, None, ('bad-color')) 368 | 369 | 370 | def test_cb_properties(): 371 | for v in ('%all', None): 372 | assert v == cli._cb_properties(None, None, v) 373 | 374 | props = 'PROP1,PROP2,PROP3' 375 | assert props.split(',') == cli._cb_properties(None, None, props) 376 | 377 | 378 | def test_cb_multiple_default(): 379 | values = ('1', '2') 380 | assert values == cli._cb_multiple_default(None, None, values) 381 | values = '1' 382 | assert (values) == cli._cb_multiple_default(None, None, values) 383 | 384 | 385 | def test_cb_bbox(poly_file): 386 | 387 | with fio.open(poly_file) as src: 388 | 389 | assert None == cli._cb_bbox(None, None, None) 390 | assert src.bounds == cli._cb_bbox(None, None, src.bounds) 391 | 392 | # Bbox with invalid X values 393 | with pytest.raises(click.BadParameter): 394 | cli._cb_bbox(None, None, (2, 0, 1, 0,)) 395 | 396 | # Bbox with invalid Y values 397 | with pytest.raises(click.BadParameter): 398 | cli._cb_bbox(None, None, (0, 2, 0, 1)) 399 | 400 | # Bbox with invalid X and Y values 401 | with pytest.raises(click.BadParameter): 402 | cli._cb_bbox(None, None, (2, 2, 1, 1,)) 403 | 404 | 405 | def test_with_emoji(runner, poly_file, line_file): 406 | result = runner.invoke(cli.main, [ 407 | poly_file, 408 | line_file, 409 | '-c', ':water_wave:', 410 | '-c', ':+1:' 411 | ]) 412 | assert result.exit_code is 0 413 | for c in (':water_wave:', ':+1:'): 414 | ucode = emoji.unicode_codes.EMOJI_ALIAS_UNICODE[c] 415 | assert ucode in result.output 416 | 417 | 418 | def test_no_style(runner, expected_polygon_40_wide, poly_file, compare_ascii): 419 | result = runner.invoke(cli.main, [ 420 | poly_file, 421 | '-c', '+', 422 | '--no-style', 423 | '-w', '40', 424 | '-f', '.' 425 | ]) 426 | assert result.exit_code is 0 427 | assert compare_ascii(result.output.strip(), expected_polygon_40_wide) 428 | 429 | 430 | def test_print_colors(runner): 431 | result = runner.invoke(cli.main, [ 432 | '--colors' 433 | ]) 434 | assert result.exit_code is 0 435 | for color in gj2ascii.DEFAULT_COLOR_CHAR.keys(): 436 | assert color in result.output 437 | -------------------------------------------------------------------------------- /tests/test_core.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unittests for gj2ascii.core 3 | """ 4 | 5 | 6 | from collections import OrderedDict 7 | import itertools 8 | import os 9 | import unittest 10 | 11 | import emoji 12 | import fiona as fio 13 | import pytest 14 | 15 | import gj2ascii 16 | import gj2ascii.core 17 | import numpy as np 18 | 19 | 20 | @pytest.fixture(scope='function') 21 | def geo_interface_feature(): 22 | 23 | class GIFeature(object): 24 | __geo_interface__ = { 25 | 'type': 'Feature', 26 | 'properties': {}, 27 | 'geometry': { 28 | 'type': 'Point', 29 | 'coordinates': [10, 20, 30] 30 | } 31 | } 32 | return GIFeature() 33 | 34 | 35 | @pytest.fixture(scope='function') 36 | def geo_interface_geometry(): 37 | class GIGeometry(object): 38 | __geo_interface__ = { 39 | 'type': 'Polygon', 40 | 'coordinates': [[(1.23, -56.5678), (4.897, 20.937), (9.9999999, -23.45)]] 41 | } 42 | return GIGeometry() 43 | 44 | 45 | @pytest.fixture(scope='function') 46 | def feature(): 47 | return { 48 | 'type': 'Feature', 49 | 'properties': {}, 50 | 'geometry': { 51 | 'type': 'Line', 52 | 'coordinates': ((1.23, -67.345), (87.12354, -23.4555), (123.876, -78.9444)) 53 | } 54 | } 55 | 56 | 57 | @pytest.fixture(scope='function') 58 | def geometry(): 59 | return { 60 | 'type': 'Point', 61 | 'coordinates': (0, 0, 10) 62 | } 63 | 64 | 65 | @pytest.fixture(scope='function') 66 | def ascii(): 67 | return os.linesep.join([ 68 | '* * * * *', 69 | ' * * ', 70 | '* * * * *' 71 | ]) 72 | 73 | 74 | @pytest.fixture(scope='function') 75 | def array(): 76 | return [ 77 | ['*', '*', '*', '*', '*'], 78 | [' ', '*', ' ', '*', ' '], 79 | ['*', '*', '*', '*', '*']] 80 | 81 | 82 | @pytest.fixture(scope='function') 83 | def np_array(array): 84 | return np.array(array) 85 | 86 | 87 | def test_compare_ascii(compare_ascii): 88 | block = """ 89 | line1 90 | line2 91 | something 92 | a n o t h e r line 93 | None 94 | 6789.2349 95 | """ 96 | assert compare_ascii(block, block) is True 97 | 98 | 99 | def test_dict2table_empty_dict(): 100 | with pytest.raises(ValueError): 101 | gj2ascii.dict2table({}) 102 | 103 | 104 | def test_dict2table(): 105 | test_dict = OrderedDict(( 106 | ('Field1', None), 107 | ('__something', 'a string'), 108 | ('more', 12345), 109 | ('other', 1.2344566) 110 | )) 111 | expected = """ 112 | +-------------+-----------+ 113 | | Field1 | None | 114 | | __something | a string | 115 | | more | 12345 | 116 | | other | 1.2344566 | 117 | +-------------+-----------+ 118 | """.strip() 119 | assert gj2ascii.dict2table(test_dict) == expected 120 | 121 | 122 | def test_render_exception(): 123 | with pytest.raises(TypeError): 124 | gj2ascii.render([], None, fill='asdf') 125 | with pytest.raises(TypeError): 126 | gj2ascii.render([], None, char='asdf') 127 | with pytest.raises(ValueError): 128 | gj2ascii.render([], width=-1) 129 | 130 | 131 | def test_render_compare_bbox_given_vs_detect_collection_vs_compute_vs_as_generator(poly_file): 132 | # Easiest to compare these 3 things together since they are related 133 | with fio.open(poly_file) as src: 134 | given = gj2ascii.render(src, 15, bbox=src.bounds) 135 | computed = gj2ascii.render([i for i in src], 15) 136 | fio_collection = gj2ascii.render(src, 15) 137 | # Passing in a generator and not specifying x/y min/max requires the features to 138 | # be iterated over twice which is a problem because generators cannot be reset. 139 | # A backup of the generator should be created automatically and iterated over the 140 | # second time. 141 | generator_output = gj2ascii.render((f for f in src), 15) 142 | for pair in itertools.combinations( 143 | [given, computed, fio_collection, generator_output], 2): 144 | assert len(set(pair)) == 1 145 | 146 | 147 | def test_with_fio(expected_polygon_40_wide, poly_file): 148 | with fio.open(poly_file) as src: 149 | r = gj2ascii.render(src, width=40, fill='.', char='+', bbox=src.bounds) 150 | assert expected_polygon_40_wide == r.rstrip() 151 | 152 | 153 | def test_geometry_extractor_exceptions(): 154 | with pytest.raises(TypeError): 155 | next(gj2ascii.core._geometry_extractor([{'type': None}])) 156 | 157 | 158 | def test_single_object(geometry, feature, geo_interface_feature, geo_interface_geometry): 159 | assert geometry == next(gj2ascii.core._geometry_extractor(geometry)) 160 | assert feature['geometry'] == next(gj2ascii.core._geometry_extractor(feature)) 161 | assert geo_interface_feature.__geo_interface__['geometry'] == next(gj2ascii.core._geometry_extractor(geo_interface_feature)) 162 | assert geo_interface_geometry.__geo_interface__ == next(gj2ascii.core._geometry_extractor(geo_interface_geometry)) 163 | 164 | 165 | def test_multiple_homogeneous(geometry, feature, geo_interface_geometry, geo_interface_feature): 166 | 167 | for item in gj2ascii.core._geometry_extractor( 168 | (geometry, geometry, geometry)): 169 | assert item == geometry 170 | 171 | for item in gj2ascii.core._geometry_extractor( 172 | (feature, feature, feature)): 173 | assert item == feature['geometry'] 174 | 175 | for item in gj2ascii.core._geometry_extractor( 176 | (geo_interface_geometry, geo_interface_geometry, geo_interface_geometry)): 177 | assert item == geo_interface_geometry.__geo_interface__ 178 | 179 | for item in gj2ascii.core._geometry_extractor( 180 | (geo_interface_feature, geo_interface_feature, geo_interface_feature)): 181 | assert item == geo_interface_feature.__geo_interface__['geometry'] 182 | 183 | 184 | def test_multiple_heterogeneous(geometry, feature, geo_interface_feature, geo_interface_geometry): 185 | input_objects = (geometry, feature, geo_interface_feature, geo_interface_geometry) 186 | expected = ( 187 | geometry, feature['geometry'], 188 | geo_interface_feature.__geo_interface__['geometry'], 189 | geo_interface_geometry.__geo_interface__ 190 | ) 191 | for expected, actual in zip( 192 | expected, gj2ascii.core._geometry_extractor(input_objects)): 193 | assert expected == actual 194 | 195 | 196 | def test_standard(): 197 | 198 | l1 = gj2ascii.array2ascii([['*', '*', '*', '*', '*'], 199 | [' ', ' ', '*', ' ', ' '], 200 | ['*', '*', ' ', ' ', ' ']]) 201 | 202 | l2 = gj2ascii.array2ascii([[' ', ' ', ' ', '+', '+'], 203 | [' ', '+', ' ', ' ', ' '], 204 | [' ', ' ', '+', '+', '+']]) 205 | 206 | eo = gj2ascii.array2ascii([['*', '*', '*', '+', '+'], 207 | ['.', '+', '*', '.', '.'], 208 | ['*', '*', '+', '+', '+']]) 209 | 210 | assert gj2ascii.stack( 211 | [l1, l2], fill='.').strip(os.linesep), eo.strip(os.linesep) 212 | 213 | 214 | def test_exceptions(): 215 | # Bad fill value 216 | with pytest.raises(ValueError): 217 | gj2ascii.stack([], fill='too-long') 218 | 219 | # Input layers have different dimensions 220 | with pytest.raises(ValueError): 221 | gj2ascii.stack(['1', '1234']) 222 | 223 | 224 | def test_single_layer(compare_ascii): 225 | l1 = gj2ascii.array2ascii([['*', '*', '*', '*', '*'], 226 | [' ', ' ', '*', ' ', ' '], 227 | ['*', '*', ' ', ' ', ' ']]) 228 | 229 | assert compare_ascii(l1, gj2ascii.stack([l1])) 230 | 231 | 232 | def test_ascii2array(array, ascii): 233 | assert array == gj2ascii.ascii2array(ascii) 234 | assert np.array_equal(array, np.array(gj2ascii.ascii2array(ascii))) 235 | 236 | 237 | def test_array2ascii(ascii, array): 238 | assert ascii == gj2ascii.array2ascii(array) 239 | assert ascii == gj2ascii.array2ascii(array) 240 | 241 | 242 | def test_roundhouse(ascii, array): 243 | assert ascii == gj2ascii.array2ascii(gj2ascii.ascii2array(ascii)) 244 | assert array == gj2ascii.ascii2array(gj2ascii.array2ascii(array)) 245 | 246 | 247 | def test_style(): 248 | 249 | array = [['0', '0', '0', '1', '0'], 250 | [' ', ' ', '2', '0', '1'], 251 | ['1', '1', '2', '1', '3']] 252 | colormap = { 253 | ' ': 'black', 254 | '0': 'blue', 255 | '1': 'yellow', 256 | '2': 'white', 257 | '3': 'red' 258 | } 259 | expected = [] 260 | for row in array: 261 | o_row = [] 262 | for char in row: 263 | color = gj2ascii.ANSI_COLORMAP[colormap[char]] 264 | o_row.append(color + char + ' ' + gj2ascii.core._ANSI_RESET) 265 | expected.append(''.join(o_row)) 266 | expected = os.linesep.join(expected) 267 | assert expected == gj2ascii.style(gj2ascii.array2ascii(array), stylemap=colormap) 268 | 269 | 270 | def test_paginate(poly_file): 271 | 272 | char = '+' 273 | fill = '.' 274 | colormap = { 275 | '+': 'red', 276 | '.': 'black' 277 | } 278 | with fio.open(poly_file) as src1, fio.open(poly_file) as src2: 279 | for paginated_feat, feat in zip( 280 | gj2ascii.paginate(src1, char=char, fill=fill, colormap=colormap), src2): 281 | assert paginated_feat.strip() == gj2ascii.style( 282 | gj2ascii.render(feat, char=char, fill=fill), stylemap=colormap) 283 | 284 | 285 | def test_bbox_from_arbitrary_iterator(poly_file): 286 | 287 | # Python 2 doesn't give direct access to an object that can be used to check if an object 288 | # is an instance of tee 289 | pair = itertools.tee(range(10)) 290 | itertools_tee_type = pair[1].__class__ 291 | 292 | with fio.open(poly_file) as c_src, \ 293 | fio.open(poly_file) as l_src, \ 294 | fio.open(poly_file) as g_src,\ 295 | fio.open(poly_file) as expected: 296 | # Tuple element 1 is an iterable object to test and element 2 is the expected type of 297 | # the output iterator 298 | test_objects = [ 299 | (c_src, fio.Collection), 300 | ([i for i in l_src], list), 301 | ((i for i in g_src), itertools_tee_type) 302 | ] 303 | for in_obj, e_type in test_objects: 304 | bbox, iterator = gj2ascii.core.min_bbox(in_obj, return_iter=True) 305 | assert bbox == expected.bounds, \ 306 | "Bounds don't match: %s != %s" % (bbox, expected.bounds) 307 | assert isinstance(iterator, e_type), "Output iterator is %s" % iterator 308 | for e, a in zip(expected, iterator): 309 | assert e['id'] == a['id'], "%s != %s" % (e['id'], a['id']) 310 | 311 | 312 | def test_render_multiple(poly_file, line_file, point_file, compare_ascii): 313 | with fio.open(poly_file) as poly, \ 314 | fio.open(line_file) as lines, \ 315 | fio.open(point_file) as points: 316 | 317 | coords = list(poly.bounds) + list(lines.bounds) + list(points.bounds) 318 | bbox = (min(coords[0::4]), min(coords[1::4]), max(coords[2::4]), max(coords[3::4])) 319 | 320 | width = 20 321 | lyr_char_pairs = [(poly, '+'), (lines, '-'), (points, '*')] 322 | actual = gj2ascii.render_multiple(lyr_char_pairs, width, fill='#') 323 | 324 | rendered_layers = [] 325 | for l, char in lyr_char_pairs: 326 | rendered_layers.append(gj2ascii.render(l, width, fill=' ', bbox=bbox, char=char)) 327 | expected = gj2ascii.stack(rendered_layers, fill='#') 328 | 329 | assert compare_ascii(actual.strip(), expected.strip()) 330 | 331 | 332 | def test_render_exceptions(): 333 | for arg in ('fill', 'char'): 334 | with pytest.raises(ValueError): 335 | gj2ascii.render([], **{arg: 'too long'}) 336 | for w in (0, -1, -1000): 337 | with pytest.raises(ValueError): 338 | gj2ascii.render([], width=w) 339 | 340 | 341 | def test_style_multiple(poly_file, line_file, point_file): 342 | with fio.open(poly_file) as poly, \ 343 | fio.open(line_file) as lines, \ 344 | fio.open(point_file) as points: 345 | 346 | coords = list(poly.bounds) + list(lines.bounds) + list(points.bounds) 347 | bbox = (min(coords[0::4]), min(coords[1::4]), max(coords[2::4]), max(coords[3::4])) 348 | 349 | width = 20 350 | 351 | # Mix of colors and emoji with a color fill 352 | lyr_color_pairs = [(poly, ':+1:'), (lines, 'blue'), (points, 'red')] 353 | actual = gj2ascii.style_multiple( 354 | lyr_color_pairs, fill='yellow', width=width, bbox=bbox) 355 | assert emoji.unicode_codes.EMOJI_ALIAS_UNICODE[':+1:'] in actual 356 | assert '\x1b[34m\x1b[44m' in actual # blue 357 | assert '\x1b[31m\x1b[41m' in actual # red 358 | assert '\x1b[33m\x1b[43m' in actual # yellow 359 | 360 | # Same as above but single character fill 361 | lyr_color_pairs = [(poly, ':+1:'), (lines, 'blue'), (points, 'red')] 362 | actual = gj2ascii.style_multiple( 363 | lyr_color_pairs, fill='.', width=width, bbox=bbox) 364 | assert emoji.unicode_codes.EMOJI_ALIAS_UNICODE[':+1:'] in actual 365 | assert '\x1b[34m\x1b[44m' in actual # blue 366 | assert '\x1b[31m\x1b[41m' in actual # red 367 | assert '.' in actual 368 | 369 | # Same as above but emoji fill 370 | lyr_color_pairs = [(poly, ':+1:'), (lines, 'blue'), (points, 'red')] 371 | actual = gj2ascii.style_multiple( 372 | lyr_color_pairs, fill=':water_wave:', width=width, bbox=bbox) 373 | assert emoji.unicode_codes.EMOJI_ALIAS_UNICODE[':+1:'] in actual 374 | assert '\x1b[34m\x1b[44m' in actual # blue 375 | assert '\x1b[31m\x1b[41m' in actual # red 376 | assert emoji.unicode_codes.EMOJI_ALIAS_UNICODE[':water_wave:'] in actual 377 | --------------------------------------------------------------------------------