Repository: mapbox/mapbox-sdk-py

Issue:
Static Map API

Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
index d99e41d..e323672 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -15,6 +15,7 @@ env:
 install:
   - pip install -U pip --cache-dir $HOME/.pip-cache
   - pip install -e .[test] --cache-dir $HOME/.pip-cache
+  - if [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install ordereddict; fi
 script:
   - py.test --cov mapbox --cov-report term-missing
   - py.test --doctest-glob='*.md' docs/*.md
diff --git a/README.rst b/README.rst
index 61fc790..e15a4c9 100644
--- a/README.rst
+++ b/README.rst
@@ -31,6 +31,11 @@ Services
   - Forward (place names ⇢ longitude, latitude)
   - Reverse (longitude, latitude ⇢ place names)
 
+- `Static Maps <https://www.mapbox.com/developers/api/static/>`__
+
+  - Generate standalone images from existing Mapbox mapids
+  - Render with GeoJSON overlays
+
 - `Surface <https://www.mapbox.com/developers/api/surface/>`__
 
   - Interpolates values along lines. Useful for elevation traces.
@@ -138,6 +143,20 @@ which returns::
     
 See ``import mapbox; help(mapbox.Distance)`` for more detailed usage.
 
+Static Maps
+-----------
+Static maps are standalone images that can be displayed on web and mobile devices without the aid of a mapping library or API. Static maps can display GeoJSON overlays and the `simplestyle-spec <https://github.com/mapbox/simplestyle-spec>`_ styles will be respected and rendered.
+
+.. code:: python
+
+    from mapbox import Static
+    res = Static().image('mapbox.satellite',
+                         lon=-61.7, lat=12.1, z=12,
+                         features=list_of_points)
+
+    with open('map.png', 'wb') as output:
+        output.write(res.content)
+
 
 Surface
 -------
diff --git a/mapbox/__init__.py b/mapbox/__init__.py
index 5743280..5decf79 100644
--- a/mapbox/__init__.py
+++ b/mapbox/__init__.py
@@ -7,3 +7,4 @@ from .services.distance import Distance
 from .services.geocoding import Geocoder, InvalidPlaceTypeError
 from .services.surface import Surface
 from .services.uploads import Uploader
+from .services.static import Static
diff --git a/mapbox/services/static.py b/mapbox/services/static.py
new file mode 100644
index 0000000..8e9febe
--- /dev/null
+++ b/mapbox/services/static.py
@@ -0,0 +1,58 @@
+import json
+
+from uritemplate import URITemplate
+
+from mapbox.services.base import Service
+
+
+class Static(Service):
+
+    def __init__(self, access_token=None):
+        self.baseuri = 'https://api.mapbox.com/v4'
+        self.session = self.get_session(access_token)
+
+    def image(self, mapid, lon=None, lat=None, z=None, features=None,
+              width=600, height=600, image_format='png256'):
+
+        if lon and lat and z:
+            auto = False
+        else:
+            auto = True
+
+        values = dict(
+            mapid=mapid,
+            lon=str(lon),
+            lat=str(lat),
+            z=str(z),
+            width=str(width),
+            height=str(height),
+            format=image_format)
+
+        if features:
+            values['overlay'] = json.dumps({'type': 'FeatureCollection',
+                                            'features': features})
+
+            if len(values['overlay']) > 4087:  # limit is 4096 minus the 'geojson()'
+                raise ValueError("geojson is too large for the static maps API, "
+                                 "must be less than 4096 characters")
+
+            if auto:
+                uri = URITemplate(
+                    '%s/{mapid}/geojson({overlay})/auto/{width}x{height}.{format}' %
+                    self.baseuri).expand(**values)
+            else:
+                uri = URITemplate(
+                    '%s/{mapid}/geojson({overlay})/{lon},{lat},{z}/{width}x{height}.{format}' %
+                    self.baseuri).expand(**values)
+        else:
+            if auto:
+                raise ValueError("Must provide features if lat, lon, z are None")
+
+            # No overlay
+            uri = URITemplate(
+                '%s/{mapid}/{lon},{lat},{z}/{width}x{height}.{format}' %
+                self.baseuri).expand(**values)
+
+        res = self.session.get(uri)
+        self.handle_http_error(res)
+        return res


Test patch:
diff --git a/tests/test_staticmaps.py b/tests/test_staticmaps.py
new file mode 100644
index 0000000..548ab2f
--- /dev/null
+++ b/tests/test_staticmaps.py
@@ -0,0 +1,99 @@
+import json
+
+try:
+    from urllib import quote
+except ImportError:
+    # python 3
+    from urllib.parse import quote
+
+try:
+    from collections import OrderedDict
+except ImportError:
+    # python 2.6
+    from ordereddict import OrderedDict
+
+import pytest
+import responses
+
+import mapbox
+
+
+@pytest.fixture
+def points():
+    points = [
+        OrderedDict(
+            type="Feature",
+            properties=OrderedDict(title="point1"),
+            geometry=OrderedDict(
+                type="Point",
+                coordinates=[-61.7, 12.1])),
+        OrderedDict(
+            type="Feature",
+            properties=OrderedDict(title="point2"),
+            geometry=OrderedDict(
+                type="Point",
+                coordinates=[-61.6, 12.0]))]
+
+    return points
+
+
+@responses.activate
+def test_staticmap_lonlatz_only():
+
+    responses.add(
+        responses.GET,
+        'https://api.mapbox.com/v4/mapbox.satellite/-61.7,12.1,12/600x600.png256?access_token=pk.test',
+        match_querystring=True,
+        body='png123',
+        status=200,
+        content_type='image/png')
+
+    res = mapbox.Static(access_token='pk.test').image('mapbox.satellite', -61.7, 12.1, 12)
+    assert res.status_code == 200
+
+
+@responses.activate
+def test_staticmap_lonlatz_features(points):
+
+    overlay = json.dumps({'type': 'FeatureCollection',
+                          'features': points})
+    overlay = quote(overlay)
+    url = ('https://api.mapbox.com/v4/mapbox.satellite/geojson({0})/'
+           '-61.7,12.1,12/600x600.png256?access_token=pk.test'.format(overlay))
+
+    responses.add(
+        responses.GET, url,
+        match_querystring=True,
+        body='png123',
+        status=200,
+        content_type='image/png')
+
+    res = mapbox.Static(access_token='pk.test').image('mapbox.satellite',
+                                                          -61.7, 12.1, 12,
+                                                          points)
+    assert res.status_code == 200
+
+@responses.activate
+def test_staticmap_auto_features(points):
+
+    overlay = json.dumps({'type': 'FeatureCollection',
+                          'features': points})
+    overlay = quote(overlay)
+    url = ('https://api.mapbox.com/v4/mapbox.satellite/geojson({0})/'
+           'auto/600x600.png256?access_token=pk.test'.format(overlay))
+
+    responses.add(
+        responses.GET, url,
+        match_querystring=True,
+        body='png123',
+        status=200,
+        content_type='image/png')
+
+    res = mapbox.Static(access_token='pk.test').image('mapbox.satellite',
+                                                          features=points)
+    assert res.status_code == 200
+
+
+def test_staticmap_auto_nofeatures(points):
+    with pytest.raises(ValueError):
+        mapbox.Static(access_token='pk.test').image('mapbox.satellite')