├── .flake8 ├── .github └── workflows │ └── test_and_release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pylintrc ├── MANIFEST ├── README.md ├── examples ├── clusters.png ├── density.png ├── heatmap.png ├── markers.png ├── polygons.png └── polyline.png ├── mapsplotlib ├── __init__.py ├── google_static_maps_api.py └── mapsplot.py ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── __init__.py └── google_static_maps_api_test.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | exclude = 4 | .git, 5 | .ipynb_checkpoints, 6 | .env*, 7 | dist 8 | -------------------------------------------------------------------------------- /.github/workflows/test_and_release.yml: -------------------------------------------------------------------------------- 1 | name: Test and Release 2 | 3 | on: 4 | push: 5 | branches: [master, dev] 6 | tags: 7 | - '*' 8 | pull_request: 9 | branches: [master, dev] 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Install Python 3.6 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: 3.6 21 | - name: Install dependencies 22 | run: | 23 | pip install --upgrade pip setuptools 24 | pip install flake8 pre-commit==2.6.0 25 | pip install -r requirements.txt 26 | - name: Lint with flake8 27 | run: flake8 . 28 | - name: Lint with pre-commit 29 | run: script -e -c "pre-commit run --all-files" 30 | - name: Unit tests 31 | run: script -e -c "nosetests -v --rednose --nologcapture --nocapture tests" 32 | 33 | release: 34 | runs-on: ubuntu-latest 35 | needs: test 36 | if: startsWith(github.ref, 'refs/tags/') 37 | 38 | steps: 39 | - uses: actions/checkout@v2 40 | - name: Install Python 3.6 41 | uses: actions/setup-python@v2 42 | with: 43 | python-version: 3.6 44 | - name: Install dependencies 45 | run: | 46 | pip install --upgrade pip setuptools 47 | pip install wheel twine 48 | - name: Build and publish to Pypi 49 | env: 50 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 51 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 52 | run: | 53 | python setup.py sdist 54 | twine upload dist/* 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore Python compiled files 8 | *.pyc 9 | 10 | # Ignore ipython notebooks & checkpoints 11 | *.ipynb 12 | .ipynb_checkpoints 13 | .env*/ 14 | dist/ 15 | 16 | *~ 17 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | - repo: git://github.com/pre-commit/pre-commit-hooks 2 | rev: v3.1.0 3 | hooks: 4 | - id: trailing-whitespace 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: debug-statements 8 | - id: check-added-large-files 9 | - id: double-quote-string-fixer 10 | - id: check-case-conflict 11 | - id: check-docstring-first 12 | - id: name-tests-test 13 | - id: requirements-txt-fixer 14 | - repo: git://github.com/pre-commit/mirrors-pylint 15 | rev: v2.5.3 16 | hooks: 17 | - id: pylint 18 | args: 19 | - --rcfile=./.pylintrc 20 | - -rn 21 | - repo: git://github.com/tcassou/python-pre-commit-hooks 22 | rev: 3383e2f83463370cf4651040fb697a636bb0374e 23 | hooks: 24 | - id: do_not_commit 25 | - id: remove_ipython_notebook_outputs 26 | - repo: https://github.com/asottile/reorder_python_imports.git 27 | rev: v2.3.4 28 | hooks: 29 | - id: reorder-python-imports 30 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Add files or directories to the blacklist. They should be base names, not 11 | # paths. 12 | ignore=CVS 13 | 14 | # Pickle collected data for later comparisons. 15 | persistent=yes 16 | 17 | # List of plugins (as comma separated values of python modules names) to load, 18 | # usually to register additional checkers. 19 | load-plugins= 20 | 21 | 22 | [MESSAGES CONTROL] 23 | 24 | # Enable the message, report, category or checker with the given id(s). You can 25 | # either give multiple identifier separated by comma (,) or put this option 26 | # multiple time. See also the "--disable" option for examples. 27 | enable=print-statement 28 | 29 | # Disable the message, report, category or checker with the given id(s). You 30 | # can either give multiple identifiers separated by comma (,) or put this 31 | # option multiple times (only on the command line, not in the configuration 32 | # file where it should appear only once).You can also use "--disable=all" to 33 | # disable everything first and then reenable specific checks. For example, if 34 | # you want to run only the similarities checker, you can use "--disable=all 35 | # --enable=similarities". If you want to run only the classes checker, but have 36 | # no Warning level messages displayed, use"--disable=all --enable=classes 37 | # --disable=W" 38 | #disable= 39 | 40 | disable=invalid-name,missing-docstring,too-many-lines,C0304,C1001,R,no-name-in-module,no-member,import-error,ungrouped-imports,wrong-import-order,star-args,protected-access,arguments-differ,signature-differs,super-init-not-called,super-on-old-class,no-init,fixme,unused-argument,abstract-method,too-many-function-args,unbalanced-tuple-unpacking 41 | 42 | 43 | [REPORTS] 44 | 45 | # Set the output format. Available formats are text, parseable, colorized, msvs 46 | # (visual studio) and html. You can also give a reporter class, eg 47 | # mypackage.mymodule.MyReporterClass. 48 | output-format=text 49 | 50 | # Put messages in a separate file for each module / package specified on the 51 | # command line instead of printing them on stdout. Reports (if any) will be 52 | # written in a file name "pylint_global.[txt|html]". 53 | files-output=no 54 | 55 | # Tells whether to display a full report or only the messages 56 | reports=yes 57 | 58 | # Python expression which should return a note less than 10 (10 is the highest 59 | # note). You have access to the variables errors warning, statement which 60 | # respectively contain the number of errors / warnings messages and the total 61 | # number of statements analyzed. This is used by the global evaluation report 62 | # (RP0004). 63 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 64 | 65 | # Template used to display messages. This is a python new-style format string 66 | # used to format the message information. See doc for all details 67 | #msg-template= 68 | 69 | 70 | [MISCELLANEOUS] 71 | 72 | # List of note tags to take in consideration, separated by a comma. 73 | notes=FIXME,XXX,TODO 74 | 75 | 76 | [BASIC] 77 | 78 | # List of builtins function names that should not be used, separated by a comma 79 | bad-functions=map,filter,apply,input 80 | 81 | # Regular expression which should only match correct module names 82 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 83 | 84 | # Regular expression which should only match correct module level names 85 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 86 | 87 | # Regular expression which should only match correct class names 88 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 89 | 90 | # Regular expression which should only match correct function names 91 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 92 | 93 | # Regular expression which should only match correct method names 94 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 95 | 96 | # Regular expression which should only match correct instance attribute names 97 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 98 | 99 | # Regular expression which should only match correct argument names 100 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 101 | 102 | # Regular expression which should only match correct variable names 103 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 104 | 105 | # Regular expression which should only match correct attribute names in class 106 | # bodies 107 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 108 | 109 | # Regular expression which should only match correct list comprehension / 110 | # generator expression variable names 111 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 112 | 113 | # Good variable names which should always be accepted, separated by a comma 114 | good-names=i,j,k,ex,Run,_ 115 | 116 | # Bad variable names which should always be refused, separated by a comma 117 | bad-names=foo,bar,baz,toto,tutu,tata 118 | 119 | # Regular expression which should only match function or class names that do 120 | # not require a docstring. 121 | no-docstring-rgx=__.*__ 122 | 123 | # Minimum line length for functions/classes that require docstrings, shorter 124 | # ones are exempt. 125 | docstring-min-length=-1 126 | 127 | 128 | [TYPECHECK] 129 | 130 | # Tells whether missing members accessed in mixin class should be ignored. A 131 | # mixin class is detected if its name ends with "mixin" (case insensitive). 132 | ignore-mixin-members=yes 133 | 134 | # List of classes names for which member attributes should not be checked 135 | # (useful for classes with attributes dynamically set). 136 | ignored-classes=SQLObject 137 | 138 | # List of members which are set dynamically and missed by pylint inference 139 | # system, and so shouldn't trigger E0201 when accessed. Python regular 140 | # expressions are accepted. 141 | generated-members=REQUEST,acl_users,aq_parent 142 | 143 | 144 | [FORMAT] 145 | 146 | # Maximum number of characters on a single line. 147 | max-line-length=120 148 | 149 | # Regexp for a line that is allowed to be longer than the limit. 150 | ignore-long-lines=^\s*(# )??$ 151 | 152 | # Allow the body of an if to be on the same line as the test if there is no 153 | # else. 154 | single-line-if-stmt=no 155 | 156 | # List of optional constructs for which whitespace checking is disabled 157 | no-space-check=trailing-comma,dict-separator 158 | 159 | # Maximum number of lines in a module 160 | max-module-lines=1000 161 | 162 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 163 | # tab). 164 | indent-string=' ' 165 | 166 | 167 | [VARIABLES] 168 | 169 | # Tells whether we should check for unused import in __init__ files. 170 | init-import=no 171 | 172 | # A regular expression matching the beginning of the name of dummy variables 173 | # (i.e. not used). 174 | dummy-variables-rgx=_$|dummy 175 | 176 | # List of additional names supposed to be defined in builtins. Remember that 177 | # you should avoid to define new builtins when possible. 178 | additional-builtins= 179 | 180 | 181 | [SIMILARITIES] 182 | 183 | # Minimum lines number of a similarity. 184 | min-similarity-lines=4 185 | 186 | # Ignore comments when computing similarities. 187 | ignore-comments=yes 188 | 189 | # Ignore docstrings when computing similarities. 190 | ignore-docstrings=yes 191 | 192 | # Ignore imports when computing similarities. 193 | ignore-imports=no 194 | 195 | 196 | [CLASSES] 197 | 198 | # List of method names used to declare (i.e. assign) instance attributes. 199 | defining-attr-methods=__init__,__new__,setUp 200 | 201 | # List of valid names for the first argument in a class method. 202 | valid-classmethod-first-arg=cls 203 | 204 | # List of valid names for the first argument in a metaclass class method. 205 | valid-metaclass-classmethod-first-arg=mcs 206 | 207 | 208 | [DESIGN] 209 | 210 | # Maximum number of arguments for function / method 211 | max-args=5 212 | 213 | # Argument names that match this expression will be ignored. Default to name 214 | # with leading underscore 215 | ignored-argument-names=_.* 216 | 217 | # Maximum number of locals for function / method body 218 | max-locals=15 219 | 220 | # Maximum number of return / yield for function / method body 221 | max-returns=6 222 | 223 | # Maximum number of branch for function / method body 224 | max-branches=12 225 | 226 | # Maximum number of statements in function / method body 227 | max-statements=50 228 | 229 | # Maximum number of parents for a class (see R0901). 230 | max-parents=7 231 | 232 | # Maximum number of attributes for a class (see R0902). 233 | max-attributes=7 234 | 235 | # Minimum number of public methods for a class (see R0903). 236 | min-public-methods=2 237 | 238 | # Maximum number of public methods for a class (see R0904). 239 | max-public-methods=20 240 | 241 | 242 | [IMPORTS] 243 | 244 | # Deprecated modules which should not be used, separated by a comma 245 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 246 | 247 | # Create a graph of every (i.e. internal and external) dependencies in the 248 | # given file (report RP0402 must not be disabled) 249 | import-graph= 250 | 251 | # Create a graph of external dependencies in the given file (report RP0402 must 252 | # not be disabled) 253 | ext-import-graph= 254 | 255 | # Create a graph of internal dependencies in the given file (report RP0402 must 256 | # not be disabled) 257 | int-import-graph= 258 | 259 | 260 | [EXCEPTIONS] 261 | 262 | # Exceptions that will emit a warning when being caught. Defaults to 263 | # "Exception" 264 | overgeneral-exceptions=Exception 265 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | setup.cfg 3 | setup.py 4 | mapsplotlib/__init__.py 5 | mapsplotlib/google_static_maps_api.py 6 | mapsplotlib/mapsplot.py 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mapsplotlib 2 | 3 | [![Build](https://github.com/tcassou/mapsplotlib/workflows/Test%20and%20Release/badge.svg)](https://github.com/tcassou/mapsplotlib/actions) 4 | 5 | Custom Python plots on a Google Maps background. A flexible matplotlib like interface to generate many types of plots on top of Google Maps. 6 | 7 | This package was renamed from the legacy `tcassou/gmaps` due to an unfortunate conflict in names with a package from Pypi. 8 | 9 | ## Setup 10 | 11 | Simply install from `pip`: 12 | ``` 13 | pip install mapsplotlib 14 | ``` 15 | 16 | You need to have a Google Static Maps API key, go to https://console.cloud.google.com/google/maps-apis, create a project, enable Google Static Maps API and get your API key. Billing details have to be enabled for your account for the API calls to succeed. 17 | Before plotting maps, you'll have to register your key (only once for each session you start): 18 | ``` 19 | from mapsplotlib import mapsplot as mplt 20 | 21 | mplt.register_api_key('your_google_api_key_here') 22 | 23 | # all plots can now be performed here 24 | ``` 25 | 26 | ## Examples 27 | 28 | ### Marker Plots 29 | 30 | Simply plotting markers on a map. Consider a pandas DataFrame `df` defined as follows: 31 | 32 | ``` 33 | | | latitude | longitude | color | size | label | 34 | |---|----------|-----------|--------|-------|-------| 35 | | 0 | 48.8770 | 2.30698 | blue | tiny | | 36 | | 1 | 48.8708 | 2.30523 | red | small | | 37 | | 2 | 48.8733 | 2.32403 | orange | mid | A | 38 | | 3 | 48.8728 | 2.30491 | black | mid | Z | 39 | | 4 | 48.8644 | 2.33160 | purple | mid | 0 | 40 | ``` 41 | 42 | Simply use (assuming `mapsplot` was imported already, and your key registered) 43 | ``` 44 | mplt.plot_markers(df) 45 | ``` 46 | will produce 47 | 48 | ![Marker Plot](https://github.com/tcassou/mapsplotlib/blob/master/examples/markers.png) 49 | 50 | ### Density Plots 51 | 52 | The only thing you need is a pandas DataFrame `df` containing a `'latitude'` and a `'longitude'` columns, describing locations. 53 | 54 | ``` 55 | mplt.density_plot(df['latitude'], df['longitude']) 56 | ``` 57 | 58 | ![Density Plot](https://github.com/tcassou/mapsplotlib/blob/master/examples/density.png) 59 | 60 | ### Heat Maps 61 | 62 | This time your pandas DataFrame `df` will need an extra `'value'` column, describing the metric you want to plot (you may have to normalize it properly for a good rendering). 63 | 64 | ``` 65 | mplt.heatmap(df['latitude'], df['longitude'], df['value']) 66 | ``` 67 | ![Heat Map](https://github.com/tcassou/mapsplotlib/blob/master/examples/heatmap.png) 68 | 69 | ### Scatter Plots 70 | 71 | Let's assume your pandas DataFrame `df` has a numerical `'cluster'` column, describing clusters of geographical points. You can produce plots like the following: 72 | 73 | ``` 74 | mplt.scatter(df['latitude'], df['longitude'], colors=df['cluster']) 75 | ``` 76 | ![Scatter Plot](https://github.com/tcassou/mapsplotlib/blob/master/examples/clusters.png) 77 | 78 | ### Polygon Plots 79 | 80 | Still with the same DataFrame `df` and its `'cluster'` column, plotting clusters and their convex hull. 81 | 82 | ``` 83 | mplt.polygons(df['latitude'], df['longitude'], df['cluster']) 84 | ``` 85 | ![Polygons Plot](https://github.com/tcassou/mapsplotlib/blob/master/examples/polygons.png) 86 | 87 | ### Polygon Plots 88 | 89 | Given a DataFrame `df` with `'latitude'` & `'longitude'` columns, plotting a line joining all `(lat, lon)` pairs (with the option to close the line). 90 | 91 | ``` 92 | mplt.polyline(df['latitude'], df['longitude'], closed=True) 93 | ``` 94 | ![Polyline Plot](https://github.com/tcassou/mapsplotlib/blob/master/examples/polyline.png) 95 | 96 | ### More to come! 97 | 98 | ## Requirements 99 | 100 | * `pandas >= 0.13.1` 101 | * `numpy >= 1.8.2` 102 | * `scipy >= 0.13.3` 103 | * `matplotlib >= 1.3.1` 104 | * `requests >= 2.7.0` 105 | * `requests>=2.18.4` 106 | * `pillow>=4.3.0` 107 | -------------------------------------------------------------------------------- /examples/clusters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tcassou/mapsplotlib/6d7773295a821ad782e132d7ef12539dc018017e/examples/clusters.png -------------------------------------------------------------------------------- /examples/density.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tcassou/mapsplotlib/6d7773295a821ad782e132d7ef12539dc018017e/examples/density.png -------------------------------------------------------------------------------- /examples/heatmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tcassou/mapsplotlib/6d7773295a821ad782e132d7ef12539dc018017e/examples/heatmap.png -------------------------------------------------------------------------------- /examples/markers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tcassou/mapsplotlib/6d7773295a821ad782e132d7ef12539dc018017e/examples/markers.png -------------------------------------------------------------------------------- /examples/polygons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tcassou/mapsplotlib/6d7773295a821ad782e132d7ef12539dc018017e/examples/polygons.png -------------------------------------------------------------------------------- /examples/polyline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tcassou/mapsplotlib/6d7773295a821ad782e132d7ef12539dc018017e/examples/polyline.png -------------------------------------------------------------------------------- /mapsplotlib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tcassou/mapsplotlib/6d7773295a821ad782e132d7ef12539dc018017e/mapsplotlib/__init__.py -------------------------------------------------------------------------------- /mapsplotlib/google_static_maps_api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from __future__ import division 4 | from __future__ import print_function 5 | from __future__ import unicode_literals 6 | 7 | try: 8 | from StringIO import StringIO 9 | except ImportError: 10 | from io import BytesIO as StringIO 11 | 12 | import numpy as np 13 | import pandas as pd 14 | import requests 15 | from PIL import Image 16 | 17 | 18 | TILE_SIZE = 256 # Basic Mercator Google Maps tile is 256 x 256 19 | MAX_SIN_LAT = 1. - 1e-5 # Bound for sinus of latitude 20 | MAX_SIZE = 640 # Max size of the map in pixels 21 | SCALE = 2 # 1 or 2 (free plan), see Google Static Maps API docs 22 | DEFAULT_ZOOM = 10 # Default zoom level, in case it cannot be determined automatically 23 | MAPTYPE = 'roadmap' # Default map type 24 | BASE_URL = 'https://maps.googleapis.com/maps/api/staticmap?' 25 | HTTP_SUCCESS_STATUS = 200 26 | HTTP_FORBIDDEN_STATUS = 403 27 | 28 | cache = {} # Caching queries to limit API calls / speed them up 29 | 30 | 31 | class GoogleStaticMapsAPI: 32 | """ 33 | 34 | API calls to the Google Static Maps API 35 | Associated transformation between geographic coordinate system / pixel location 36 | 37 | See https://developers.google.com/maps/documentation/static-maps/intro for more info. 38 | 39 | """ 40 | @classmethod 41 | def register_api_key(cls, api_key): 42 | """Register a Google Static Maps API key to enable queries to Google. 43 | Create your own Google Static Maps API key on https://console.developers.google.com. 44 | 45 | :param str api_key: the API key 46 | 47 | :return: None 48 | """ 49 | cls._api_key = api_key 50 | 51 | @classmethod 52 | def map( 53 | cls, center=None, zoom=None, size=(MAX_SIZE, MAX_SIZE), scale=SCALE, 54 | maptype=MAPTYPE, file_format='png32', markers=None): 55 | """GET query on the Google Static Maps API to retrieve a static image. 56 | 57 | :param object center: (required if markers not present) defines the center of the map, equidistant from edges. 58 | This parameter takes a location as either 59 | * a tuple of floats (latitude, longitude) 60 | * or a string address (e.g. "city hall, new york, ny") identifying a unique location 61 | 62 | :param int zoom: (required if markers not present) defines the zoom level of the map: 63 | * 1: World 64 | * 5: Landmass/continent 65 | * 10: City 66 | * 15: Streets 67 | * 20: Buildings 68 | 69 | :param (int, int) size: (required) defines the rectangular dimensions (pixels) of the map image. 70 | Max size for each dimension is 640 (free account). 71 | 72 | :param int scale: (optional), 1 or 2 (free plan). Affects the number of pixels that are returned. 73 | scale=2 returns twice as many pixels as scale=1 while retaining the same coverage area and level of detail 74 | (i.e. the contents of the map don't change). 75 | 76 | :param string maptype: (optional) defines the type of map to construct. Several possible values, including 77 | * roadmap (default): specifies a standard roadmap image, as is normally shown on the Google Maps. 78 | * satellite: specifies a satellite image. 79 | * terrain: specifies a physical relief map image, showing terrain and vegetation. 80 | * hybrid: specifies a hybrid of the satellite and roadmap image, showing a transparent layer of 81 | major streets and place names on the satellite image. 82 | 83 | :param string file_format: image format 84 | * png8 or png (default) specifies the 8-bit PNG format. 85 | * png32 specifies the 32-bit PNG format. 86 | * gif specifies the GIF format. 87 | * jpg specifies the JPEG compression format. 88 | * jpg-baseline 89 | 90 | :param {string: object} markers: points to be marked on the map, under the form of a dict with keys 91 | * 'color': (optional) 24-bit (0xFFFFCC) or predefined from 92 | {black, brown, green, purple, yellow, blue, gray, orange, red, white} 93 | * 'size': (optional) {tiny, mid, small} 94 | * 'label': (optional) specifies a single uppercase alphanumeric character from the set {A-Z, 0-9}. 95 | Only compatible with size markers 96 | * 'coordinates': list of tuples (lat, long) for which the options are common. 97 | 98 | :return: map image 99 | :rtype: PIL.Image 100 | """ 101 | 102 | # For now, caching only if no markers are given 103 | should_cache = markers is None 104 | 105 | url = BASE_URL 106 | if center: 107 | url += 'center={},{}&'.format(*center) if isinstance(center, tuple) else 'center={}&'.format(center) 108 | if zoom: 109 | url += 'zoom={}&'.format(zoom) 110 | 111 | markers = markers if markers else [] 112 | for marker in markers: 113 | if 'latitude' in marker and 'longitude' in marker: 114 | url += 'markers=' 115 | for key in ['color', 'size', 'label']: 116 | if key in marker: 117 | url += '{}:{}%7C'.format(key, marker[key]) 118 | url += '{},{}%7C'.format(marker['latitude'], marker['longitude']) 119 | url += '&' 120 | 121 | url += 'scale={}&'.format(scale) 122 | url += 'size={}x{}&'.format(*tuple(min(el, MAX_SIZE) for el in size)) 123 | url += 'maptype={}&'.format(maptype) 124 | url += 'format={}&'.format(file_format) 125 | 126 | if hasattr(cls, '_api_key'): 127 | url += 'key={}'.format(cls._api_key) 128 | 129 | if url in cache: 130 | return cache[url] 131 | 132 | response = requests.get(url) 133 | # Checking response code, in case of error adding Google API message to the debug of requests exception 134 | if response.status_code != HTTP_SUCCESS_STATUS: 135 | if response.status_code == HTTP_FORBIDDEN_STATUS: 136 | if not hasattr(cls, '_api_key'): 137 | raise KeyError('No Google Static Maps API key registered - refer to the README.') 138 | else: 139 | raise KeyError('Error {} - Forbidden. Is your API key valid?'.format(HTTP_FORBIDDEN_STATUS)) 140 | else: 141 | print('HTTPError: {} - {}.'.format(response.status_code, response.reason)) 142 | response.raise_for_status() # This raises an error in case of unexpected status code 143 | 144 | # Processing the image in case of success 145 | img = Image.open(StringIO((response.content))) 146 | if should_cache: 147 | cache[url] = img 148 | 149 | return img 150 | 151 | @classmethod 152 | def to_pixel(cls, latitude, longitude): 153 | """Transform a pair lat/long in pixel location on a world map without zoom (absolute location). 154 | 155 | :param float latitude: latitude of point 156 | :param float longitude: longitude of point 157 | 158 | :return: pixel coordinates 159 | :rtype: pandas.Series 160 | """ 161 | siny = np.clip(np.sin(latitude * np.pi / 180), -MAX_SIN_LAT, MAX_SIN_LAT) 162 | return pd.Series( 163 | [ 164 | TILE_SIZE * (0.5 + longitude / 360), 165 | TILE_SIZE * (0.5 - np.log((1 + siny) / (1 - siny)) / (4 * np.pi)), 166 | ], 167 | index=['x_pixel', 'y_pixel'], 168 | ) 169 | 170 | @classmethod 171 | def to_pixels(cls, latitudes, longitudes): 172 | """Transform a set of lat/long coordinates in pixel location on a world map without zoom (absolute location). 173 | 174 | :param pandas.Series latitudes: set of latitudes 175 | :param pandas.Series longitudes: set of longitudes 176 | 177 | :return: pixel coordinates 178 | :rtype: pandas.DataFrame 179 | """ 180 | siny = np.clip(np.sin(latitudes * np.pi / 180), -MAX_SIN_LAT, MAX_SIN_LAT) 181 | return pd.concat( 182 | [ 183 | TILE_SIZE * (0.5 + longitudes / 360), 184 | TILE_SIZE * (0.5 - np.log((1 + siny) / (1 - siny)) / (4 * np.pi)), 185 | ], 186 | axis=1, keys=['x_pixel', 'y_pixel'], 187 | ) 188 | 189 | @classmethod 190 | def to_tile_coordinates(cls, latitudes, longitudes, center_lat, center_long, zoom, size, scale): 191 | """Transform a set of lat/long coordinates into pixel position in a tile. These coordinates depend on 192 | * the zoom level 193 | * the tile location on the world map 194 | 195 | :param pandas.Series latitudes: set of latitudes 196 | :param pandas.Series longitudes: set of longitudes 197 | :param float center_lat: center of the tile (latitude) 198 | :param float center_long: center of the tile (longitude) 199 | :param int zoom: Google maps zoom level 200 | :param int size: size of the tile 201 | :param int scale: 1 or 2 (free plan), see Google Static Maps API docs 202 | 203 | :return: pixel coordinates in the tile 204 | :rtype: pandas.DataFrame 205 | """ 206 | pixels = cls.to_pixels(latitudes, longitudes) 207 | return scale * ((pixels - cls.to_pixel(center_lat, center_long)) * 2 ** zoom + size / 2) 208 | 209 | @classmethod 210 | def get_zoom(cls, latitudes, longitudes, size, scale): 211 | """Compute level of zoom needed to display all points in a single tile. 212 | 213 | :param pandas.Series latitudes: set of latitudes 214 | :param pandas.Series longitudes: set of longitudes 215 | :param int size: size of the tile 216 | :param int scale: 1 or 2 (free plan), see Google Static Maps API docs 217 | 218 | :return: zoom level 219 | :rtype: int 220 | """ 221 | # Extreme pixels 222 | min_pixel = cls.to_pixel(latitudes.min(), longitudes.min()) 223 | max_pixel = cls.to_pixel(latitudes.max(), longitudes.max()) 224 | # Longitude spans from -180 to +180, latitudes only from -90 to +90 225 | max_amplitude = ((max_pixel - min_pixel).abs() * pd.Series([2., 1.], index=['x_pixel', 'y_pixel'])).max() 226 | if max_amplitude == 0 or np.isnan(max_amplitude): 227 | return DEFAULT_ZOOM 228 | return int(np.log2(2 * size / max_amplitude)) 229 | -------------------------------------------------------------------------------- /mapsplotlib/mapsplot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from __future__ import division 4 | from __future__ import print_function 5 | from __future__ import unicode_literals 6 | 7 | import warnings 8 | 9 | import numpy as np 10 | import pandas as pd 11 | import scipy.ndimage as ndi 12 | from matplotlib import cm 13 | from matplotlib import pyplot as plt 14 | from matplotlib.collections import PatchCollection 15 | from matplotlib.patches import PathPatch 16 | from matplotlib.patches import Polygon 17 | from matplotlib.patches import Rectangle 18 | from matplotlib.path import Path 19 | from scipy.spatial import ConvexHull 20 | 21 | from .google_static_maps_api import GoogleStaticMapsAPI 22 | from .google_static_maps_api import MAPTYPE 23 | from .google_static_maps_api import MAX_SIZE 24 | from .google_static_maps_api import SCALE 25 | 26 | 27 | BLANK_THRESH = 2 * 1e-3 # Value below which point in a heatmap should be blank 28 | 29 | 30 | def register_api_key(api_key): 31 | """Register a Google Static Maps API key to enable queries to Google. 32 | Create your own Google Static Maps API key on https://console.developers.google.com. 33 | 34 | :param str api_key: the API key 35 | 36 | :return: None 37 | """ 38 | GoogleStaticMapsAPI.register_api_key(api_key) 39 | 40 | 41 | def background_and_pixels(latitudes, longitudes, size, maptype): 42 | """Queries the proper background map and translate geo coordinated into pixel locations on this map. 43 | 44 | :param pandas.Series latitudes: series of sample latitudes 45 | :param pandas.Series longitudes: series of sample longitudes 46 | :param int size: target size of the map, in pixels 47 | :param string maptype: type of maps, see GoogleStaticMapsAPI docs for more info 48 | 49 | :return: map and pixels 50 | :rtype: (PIL.Image, pandas.DataFrame) 51 | """ 52 | # From lat/long to pixels, zoom and position in the tile 53 | center_lat = (latitudes.max() + latitudes.min()) / 2 54 | center_long = (longitudes.max() + longitudes.min()) / 2 55 | zoom = GoogleStaticMapsAPI.get_zoom(latitudes, longitudes, size, SCALE) 56 | pixels = GoogleStaticMapsAPI.to_tile_coordinates(latitudes, longitudes, center_lat, center_long, zoom, size, SCALE) 57 | # Google Map 58 | img = GoogleStaticMapsAPI.map( 59 | center=(center_lat, center_long), 60 | zoom=zoom, 61 | scale=SCALE, 62 | size=(size, size), 63 | maptype=maptype, 64 | ) 65 | return img, pixels 66 | 67 | 68 | def scatter(latitudes, longitudes, colors=None, maptype=MAPTYPE, alpha=0.5): 69 | """Scatter plot over a map. Can be used to visualize clusters by providing the marker colors. 70 | 71 | :param pandas.Series latitudes: series of sample latitudes 72 | :param pandas.Series longitudes: series of sample longitudes 73 | :param pandas.Series colors: marker colors, as integers 74 | :param string maptype: type of maps, see GoogleStaticMapsAPI docs for more info 75 | :param float alpha: transparency for plot markers between 0 (transparent) and 1 (opaque) 76 | 77 | :return: None 78 | """ 79 | width = SCALE * MAX_SIZE 80 | colors = pd.Series(0, index=latitudes.index) if colors is None else colors 81 | img, pixels = background_and_pixels(latitudes, longitudes, MAX_SIZE, maptype) 82 | plt.figure(figsize=(10, 10)) 83 | plt.imshow(np.array(img)) # Background map 84 | plt.scatter( # Scatter plot 85 | pixels['x_pixel'], 86 | pixels['y_pixel'], 87 | c=colors, 88 | s=width / 40, 89 | linewidth=0, 90 | alpha=alpha, 91 | ) 92 | plt.gca().invert_yaxis() # Origin of map is upper left 93 | plt.axis([0, width, width, 0]) # Remove margin 94 | plt.axis('off') 95 | plt.tight_layout() 96 | plt.show() 97 | 98 | 99 | def plot_markers(markers, maptype=MAPTYPE): 100 | """Plot markers on a map. 101 | 102 | :param pandas.DataFrame markers: DataFrame with at least 'latitude' and 'longitude' columns, and optionally 103 | * 'color' column, see GoogleStaticMapsAPI docs for more info 104 | * 'label' column, see GoogleStaticMapsAPI docs for more info 105 | * 'size' column, see GoogleStaticMapsAPI docs for more info 106 | :param string maptype: type of maps, see GoogleStaticMapsAPI docs for more info 107 | 108 | :return: None 109 | """ 110 | # Checking input columns 111 | fields = markers.columns.intersection(['latitude', 'longitude', 'color', 'label', 'size']) 112 | if not fields or 'latitude' not in fields or 'longitude' not in fields: 113 | msg = 'Input dataframe should contain at least colums \'latitude\' and \'longitude\' ' 114 | msg += '(and columns \'color\', \'label\', \'size\' optionally).' 115 | raise KeyError(msg) 116 | # Checking NaN input 117 | nans = (markers.latitude.isnull() | markers.longitude.isnull()) 118 | if nans.sum() > 0: 119 | warnings.warn('Ignoring {} example(s) containing NaN latitude or longitude.'.format(nans.sum())) 120 | # Querying map 121 | img = GoogleStaticMapsAPI.map( 122 | scale=SCALE, 123 | markers=markers[fields].loc[~nans].T.to_dict().values(), 124 | maptype=maptype, 125 | ) 126 | plt.figure(figsize=(10, 10)) 127 | plt.imshow(np.array(img)) 128 | plt.tight_layout() 129 | plt.axis('off') 130 | plt.show() 131 | 132 | 133 | def heatmap(latitudes, longitudes, values, resolution=None, maptype=MAPTYPE, alpha=0.25): 134 | """Plot a geographical heatmap of the given metric. 135 | 136 | :param pandas.Series latitudes: series of sample latitudes 137 | :param pandas.Series longitudes: series of sample longitudes 138 | :param pandas.Series values: series of sample values 139 | :param int resolution: resolution (in pixels) for the heatmap 140 | :param string maptype: type of maps, see GoogleStaticMapsAPI docs for more info 141 | :param float alpha: transparency for heatmap overlay, between 0 (transparent) and 1 (opaque) 142 | 143 | :return: None 144 | """ 145 | img, pixels = background_and_pixels(latitudes, longitudes, MAX_SIZE, maptype) 146 | # Smooth metric 147 | z = grid_density_gaussian_filter( 148 | zip(pixels['x_pixel'], pixels['y_pixel'], values), 149 | MAX_SIZE * SCALE, 150 | resolution=resolution if resolution else MAX_SIZE * SCALE, # Heuristic for pretty plots 151 | ) 152 | # Plot 153 | width = SCALE * MAX_SIZE 154 | plt.figure(figsize=(10, 10)) 155 | plt.imshow(np.array(img)) # Background map 156 | plt.imshow(z, origin='lower', extent=[0, width, 0, width], alpha=alpha) # Foreground, transparent heatmap 157 | plt.scatter(pixels['x_pixel'], pixels['y_pixel'], s=1) # Markers of all points 158 | plt.gca().invert_yaxis() # Origin of map is upper left 159 | plt.axis([0, width, width, 0]) # Remove margin 160 | plt.axis('off') 161 | plt.tight_layout() 162 | plt.show() 163 | 164 | 165 | def density_plot(latitudes, longitudes, resolution=None, maptype=MAPTYPE, alpha=0.25): 166 | """Given a set of geo coordinates, draw a density plot on a map. 167 | 168 | :param pandas.Series latitudes: series of sample latitudes 169 | :param pandas.Series longitudes: series of sample longitudes 170 | :param int resolution: resolution (in pixels) for the heatmap 171 | :param string maptype: type of maps, see GoogleStaticMapsAPI docs for more info 172 | :param float alpha: transparency for heatmap overlay, between 0 (transparent) and 1 (opaque) 173 | 174 | :return: None 175 | """ 176 | heatmap(latitudes, longitudes, np.ones(latitudes.shape[0]), resolution=resolution, maptype=maptype, alpha=alpha) 177 | 178 | 179 | def grid_density_gaussian_filter(data, size, resolution=None, smoothing_window=None): 180 | """Smoothing grid values with a Gaussian filter. 181 | 182 | :param [(float, float, float)] data: list of 3-dimensional grid coordinates 183 | :param int size: grid size 184 | :param int resolution: desired grid resolution 185 | :param int smoothing_window: size of the gaussian kernels for smoothing 186 | 187 | :return: smoothed grid values 188 | :rtype: numpy.ndarray 189 | """ 190 | resolution = resolution if resolution else size 191 | k = (resolution - 1) / size 192 | w = smoothing_window if smoothing_window else int(0.01 * resolution) # Heuristic 193 | imgw = (resolution + 2 * w) 194 | img = np.zeros((imgw, imgw)) 195 | for x, y, z in data: 196 | ix = int(x * k) + w 197 | iy = int(y * k) + w 198 | if 0 <= ix < imgw and 0 <= iy < imgw: 199 | img[iy][ix] += z 200 | z = ndi.gaussian_filter(img, (w, w)) # Gaussian convolution 201 | z[z <= BLANK_THRESH] = np.nan # Making low values blank 202 | return z[w:-w, w:-w] 203 | 204 | 205 | def polygons(latitudes, longitudes, clusters, maptype=MAPTYPE, alpha=0.25): 206 | """Plot clusters of points on map, including them in a polygon defining their convex hull. 207 | 208 | :param pandas.Series latitudes: series of sample latitudes 209 | :param pandas.Series longitudes: series of sample longitudes 210 | :param pandas.Series clusters: marker clusters 211 | :param string maptype: type of maps, see GoogleStaticMapsAPI docs for more info 212 | :param float alpha: transparency for polygons overlay, between 0 (transparent) and 1 (opaque) 213 | 214 | :return: None 215 | """ 216 | width = SCALE * MAX_SIZE 217 | img, pixels = background_and_pixels(latitudes, longitudes, MAX_SIZE, maptype) 218 | 219 | # Building collection of polygons 220 | polygon_list = [] 221 | unique_clusters = clusters.unique() 222 | cmap = pd.Series(np.arange(unique_clusters.shape[0] - 1, -1, -1), index=unique_clusters) 223 | for c in unique_clusters: 224 | in_polygon = clusters == c 225 | if in_polygon.sum() < 3: 226 | print('[WARN] Cannot draw polygon for cluster {} - only {} samples.'.format(c, in_polygon.sum())) 227 | continue 228 | cluster_pixels = pixels.loc[clusters == c] 229 | polygon_list.append(Polygon(cluster_pixels.iloc[ConvexHull(cluster_pixels).vertices], closed=True)) 230 | 231 | # Background map 232 | plt.figure(figsize=(10, 10)) 233 | ax = plt.subplot(111) 234 | plt.imshow(np.array(img)) 235 | # Collection of polygons 236 | p = PatchCollection(polygon_list, cmap='jet', alpha=alpha) 237 | p.set_array(cmap.values) 238 | ax.add_collection(p) 239 | # Scatter plot 240 | plt.scatter( 241 | pixels['x_pixel'], 242 | pixels['y_pixel'], 243 | c=cmap.loc[clusters], 244 | cmap='jet', 245 | s=width / 40, 246 | linewidth=0, 247 | alpha=alpha, 248 | ) 249 | # Axis options 250 | plt.gca().invert_yaxis() # Origin of map is upper left 251 | plt.axis([0, width, width, 0]) # Remove margin 252 | plt.axis('off') 253 | plt.tight_layout() 254 | # Building legend box 255 | jet_cmap = cm.get_cmap('jet') 256 | plt.legend( 257 | [Rectangle((0, 0), 1, 1, fc=jet_cmap(i / max(cmap.shape[0] - 1, 1)), alpha=alpha) for i in cmap.values], 258 | cmap.index, 259 | loc=4, 260 | bbox_to_anchor=(1.1, 0), 261 | ) 262 | plt.show() 263 | 264 | 265 | def polyline(latitudes, longitudes, closed=False, maptype=MAPTYPE, alpha=1.): 266 | """Plot a polyline on a map, joining (lat lon) pairs in the order defined by the input. 267 | 268 | :param pandas.Series latitudes: series of sample latitudes 269 | :param pandas.Series longitudes: series of sample longitudes 270 | :param bool closed: set to `True` if you want to close the line, from last (lat, lon) pair back to first one 271 | :param string maptype: type of maps, see GoogleStaticMapsAPI docs for more info 272 | :param float alpha: transparency for polyline overlay, between 0 (transparent) and 1 (opaque) 273 | 274 | :return: None 275 | """ 276 | width = SCALE * MAX_SIZE 277 | img, pixels = background_and_pixels(latitudes, longitudes, MAX_SIZE, maptype) 278 | # Building polyline 279 | verts = pixels.values.tolist() 280 | codes = [Path.MOVETO] + [Path.LINETO for _ in range(max(pixels.shape[0] - 1, 0))] 281 | if closed: 282 | verts.append(verts[0]) 283 | codes.append(Path.CLOSEPOLY) 284 | # Background map 285 | plt.figure(figsize=(10, 10)) 286 | ax = plt.subplot(111) 287 | plt.imshow(np.array(img)) 288 | # Polyline 289 | patch = PathPatch(Path(verts, codes), facecolor='none', lw=2, alpha=alpha) 290 | ax.add_patch(patch) 291 | # Axis options 292 | plt.gca().invert_yaxis() # Origin of map is upper left 293 | plt.axis([0, width, width, 0]) # Remove margin 294 | plt.axis('off') 295 | plt.tight_layout() 296 | plt.show() 297 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | genty>=1.3.2 2 | matplotlib>=1.3.1 3 | nose>=1.3.7 4 | numpy>=1.8.2 5 | pandas>=0.13.1 6 | pillow>=4.3.0 7 | rednose>=1.3.0 8 | requests>=2.18.4 9 | scipy>=0.13.3 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from distutils.core import setup 3 | 4 | 5 | version = '1.2.1' 6 | 7 | setup( 8 | name='mapsplotlib', 9 | packages=['mapsplotlib'], 10 | version=version, 11 | description=''' 12 | Custom Python plots on a Google Maps background. 13 | A flexible matplotlib like interface to generate many types of plots on top of Google Maps. 14 | ''', 15 | url='https://github.com/tcassou/mapsplotlib', 16 | download_url='https://github.com/tcassou/mapsplotlib/archive/{}.tar.gz'.format(version), 17 | keywords=['google', 'maps', 'matplotlib', 'python', 'plot'], 18 | classifiers=[ 19 | 'Programming Language :: Python', 20 | 'Intended Audience :: Developers', 21 | 'Programming Language :: Python :: 2.7', 22 | 'Programming Language :: Python :: 3.6', 23 | 'Topic :: Scientific/Engineering :: Visualization', 24 | ], 25 | install_requires=[ 26 | 'numpy>=1.8.2', 27 | 'pandas>=0.13.1', 28 | 'scipy>=0.13.3', 29 | 'matplotlib>=1.3.1', 30 | 'requests>=2.18.4', 31 | 'pillow>=4.3.0', 32 | ], 33 | ) 34 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tcassou/mapsplotlib/6d7773295a821ad782e132d7ef12539dc018017e/tests/__init__.py -------------------------------------------------------------------------------- /tests/google_static_maps_api_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from __future__ import division 4 | from __future__ import print_function 5 | from __future__ import unicode_literals 6 | 7 | import unittest 8 | 9 | import numpy as np 10 | import pandas as pd 11 | from genty import genty 12 | from genty import genty_dataset 13 | from nose.tools import eq_ 14 | from nose.tools import ok_ 15 | from numpy.testing import assert_array_almost_equal 16 | from numpy.testing import assert_array_equal 17 | 18 | from mapsplotlib.google_static_maps_api import GoogleStaticMapsAPI 19 | # from numpy.testing import assert_raises 20 | 21 | 22 | @genty 23 | class GoogleStaticMapsAPITest(unittest.TestCase): 24 | 25 | @genty_dataset( 26 | some_key=('foobarbaz',), 27 | ) 28 | def test_register_api_key(self, key): 29 | ok_(not hasattr(GoogleStaticMapsAPI, '_api_key')) 30 | GoogleStaticMapsAPI.register_api_key(key) 31 | ok_(hasattr(GoogleStaticMapsAPI, '_api_key')) 32 | eq_(GoogleStaticMapsAPI._api_key, key) 33 | 34 | # For now it queries without an API key seem to be allowed still 35 | # def test_query_without_api_key(self): 36 | # assert_raises(KeyError, GoogleStaticMapsAPI.map) 37 | 38 | @genty_dataset( 39 | amsterdam=(52.3702, 4.8952, 131.481031, 84.131289), 40 | paris=(48.8566, 2.3522, 129.672676, 88.071271), 41 | nan_lat=(np.nan, 4.8952, 131.481031, np.nan), 42 | nan_lon=(52.3702, np.nan, np.nan, 84.131289), 43 | all_nan=(np.nan, np.nan, np.nan, np.nan), 44 | ) 45 | def test_to_pixel(self, latitude, longitude, x_pixel, y_pixel): 46 | pixels = GoogleStaticMapsAPI.to_pixel(latitude, longitude) 47 | ok_(isinstance(pixels, pd.Series)) 48 | assert_array_equal(pixels.index, ['x_pixel', 'y_pixel']) 49 | assert_array_almost_equal(pixels.values, [x_pixel, y_pixel], decimal=6) 50 | 51 | @genty_dataset( 52 | one_entry=([52.3702], [4.8952], [[131.481031, 84.131289]]), 53 | multiple_entries=([52.3702, 48.8566], [4.8952, 2.3522], [[131.481031, 84.131289], [129.672676, 88.071271]]), 54 | one_lat_nan=([52.3702, np.nan], [4.8952, 2.3522], [[131.481031, 84.131289], [129.672676, np.nan]]), 55 | one_lon_nan=([52.3702, 48.8566], [np.nan, 2.3522], [[np.nan, 84.131289], [129.672676, 88.071271]]), 56 | one_row_nan=([np.nan, 48.8566], [np.nan, 2.3522], [[np.nan, np.nan], [129.672676, 88.071271]]), 57 | ) 58 | def test_to_pixels(self, latitudes, longitudes, expected): 59 | pixels = GoogleStaticMapsAPI.to_pixels(pd.Series(latitudes), pd.Series(longitudes)) 60 | ok_(isinstance(pixels, pd.DataFrame)) 61 | assert_array_equal(pixels.index, range(len(latitudes))) 62 | assert_array_equal(pixels.columns, ['x_pixel', 'y_pixel']) 63 | assert_array_almost_equal(pixels, expected, decimal=6) 64 | 65 | @genty_dataset( 66 | one_entry=([52.3702], [4.8952], 52.3702, 4.8952, 10, 640, 2, [[640.0, 640.0]]), 67 | one_entry_some_zoom=([52.3702], [4.8952], 52.3702, 4.8952, 6, 640, 2, [[640.0, 640.0]]), 68 | one_entry_some_size=([52.3702], [4.8952], 52.3702, 4.8952, 10, 400, 2, [[400.0, 400.0]]), 69 | one_entry_some_scale=([52.3702], [4.8952], 52.3702, 4.8952, 10, 640, 1, [[320.0, 320.0]]), 70 | multiple_entries=( 71 | [52.3702, 48.8566], [4.8952, 2.3522], 50.61, 3.62, 8, 640, 2, 72 | [[1104.3, -389.4], [178.4, 1627.8]] 73 | ), 74 | multiple_entries_some_zoom=( 75 | [52.3702, 48.8566], [4.8952, 2.3522], 50.61, 3.62, 5, 640, 2, 76 | [[698., 511.3], [582.3, 763.5]] 77 | ), 78 | multiple_entries_some_size=( 79 | [52.3702, 48.8566], [4.8952, 2.3522], 50.61, 3.62, 8, 320, 2, 80 | [[784.3, -709.4], [-141.6, 1307.8]] 81 | ), 82 | multiple_entries_some_scale=( 83 | [52.3702, 48.8566], [4.8952, 2.3522], 50.61, 3.62, 8, 640, 1, 84 | [[552.1, -194.7], [89.2, 813.9]] 85 | ), 86 | one_lat_nan=( 87 | [np.nan, 48.8566], [4.8952, 2.3522], 48.8566, 3.62, 8, 640, 2, 88 | [[1104.3, np.nan], [178.4, 640.]] 89 | ), 90 | one_lon_nan=( 91 | [52.3702, 48.8566], [np.nan, 2.3522], 50.61, 2.3522, 8, 640, 2, 92 | [[np.nan, -389.4], [640., 1627.8]] 93 | ), 94 | one_row_nan=( 95 | [52.3702, np.nan], [4.8952, np.nan], 52.3702, 4.8952, 8, 640, 2, 96 | [[640., 640.], [np.nan, np.nan]] 97 | ), 98 | ) 99 | def test_to_tile_coordinates(self, latitudes, longitudes, center_lat, center_lon, zoom, size, scale, expected): 100 | coords = GoogleStaticMapsAPI.to_tile_coordinates( 101 | pd.Series(latitudes), pd.Series(longitudes), center_lat, center_lon, zoom, size, scale) 102 | ok_(isinstance(coords, pd.DataFrame)) 103 | assert_array_equal(coords.index, range(len(latitudes))) 104 | assert_array_equal(coords.columns, ['x_pixel', 'y_pixel']) 105 | assert_array_almost_equal(coords, expected, decimal=1) 106 | 107 | @genty_dataset( 108 | one_entry=([52.3702], [4.8952], 640, 2, 10), 109 | one_entry_some_size=([52.3702], [4.8952], 238, 2, 10), 110 | one_entry_some_scale=([52.3702], [4.8952], 640, 1, 10), 111 | multiple_entries=([52.3702, 48.8566], [4.8952, 2.3522], 640, 2, 8), 112 | multiple_entries_some_size=([52.3702, 48.8566], [4.8952, 2.3522], 430, 2, 7), 113 | multiple_entries_some_scale=([52.3702, 48.8566], [4.8952, 2.3522], 640, 1, 8), 114 | one_lat_nan=([np.nan, 48.8566], [4.8952, 2.3522], 640, 2, 8), 115 | one_lon_nan=([52.3702, 48.8566], [np.nan, 2.3522], 640, 2, 8), 116 | one_row_nan=([np.nan, 48.8566], [np.nan, 2.3522], 640, 2, 10), 117 | no_entry=([], [], 640, 2, 10), 118 | ) 119 | def test_get_zoom(self, latitudes, longitudes, size, scale, expected): 120 | eq_(GoogleStaticMapsAPI.get_zoom(pd.Series(latitudes), pd.Series(longitudes), size, scale), expected) 121 | --------------------------------------------------------------------------------