Repository: mapbox/mapbox-sdk-py

Issue:
Map Matching API

Discussion:

Patch:
diff --git a/docs/mapmatching.md b/docs/mapmatching.md
new file mode 100644
index 0000000..f404714
--- /dev/null
+++ b/docs/mapmatching.md
@@ -0,0 +1,89 @@
+# Map Matching
+
+The `MapMatcher` class from the `mapbox.services.mapmatching` module provides
+access to the Mapbox Map Matching API. You can also import it directly from the
+`mapbox` module.
+
+```python
+>>> from mapbox import MapMatcher
+
+```
+
+See https://www.mapbox.com/developers/api/map-matching/ for general documentation
+of the API.
+
+Your Mapbox access token should be set in your environment; see the [access
+tokens](access_tokens.md) documentation for more information.
+
+## MapMatcher methods
+
+The methods of the `MapMatcher` class return an instance of
+[`requests.Response`](http://docs.python-requests.org/en/latest/api/#requests.Response).
+
+In addition to the `json()` method that returns Python data parsed from the
+API, the responses provide a `geojson()` method that converts that
+data to a GeoJSON like form.
+
+## Usage
+
+The Mapbox Map Matching API lets you take recorded GPS traces and snap them to the OpenStreetMap road and path network. This is helpful for aligning noisy traces and displaying them cleanly on a map.
+
+The Map Matching API is limited to 60 requests per minute and results must be displayed on a Mapbox map using one of our SDKs. For high volume or other use cases, contact us.
+
+
+```python
+>>> service = MapMatcher()
+
+```
+
+The input data to the Map Matcher must be a single GeoJSON-like Feature with a LineString geometry.
+The optional `coordTimes` property should be an array of the same length as the coordinates
+containing timestamps to help make the matching more accurate.
+
+```
+>>> line = {
+...     "type": "Feature",
+...     "properties": {
+...         "coordTimes": [
+...             "2015-04-21T06:00:00Z",
+...             "2015-04-21T06:00:05Z",
+...             "2015-04-21T06:00:10Z",
+...             "2015-04-21T06:00:15Z",
+...             "2015-04-21T06:00:20Z"]},
+...     "geometry": {
+...         "type": "LineString",
+...         "coordinates": [
+...             [13.418946862220764, 52.50055852688439],
+...             [13.419011235237122, 52.50113000479732],
+...             [13.419756889343262, 52.50171780290061],
+...             [13.419885635375975, 52.50237416816131],
+...             [13.420631289482117, 52.50294888790448]]}}
+
+```
+
+Use the `surface()` method to query the terrain dataset.
+
+```python
+>>> response = service.match(line, profile='mapbox.driving')
+>>> response.status_code
+200
+>>> response.headers['Content-Type']
+'application/json; charset=utf-8'
+
+```
+
+The response geojson contains a FeatureCollection with a single feature,
+with the new LineString corrected to match segments from the selected profile.
+
+```python
+>>> corrected = response.geojson()['features'][0]
+>>> corrected['geometry']['type']
+'LineString'
+>>> corrected['geometry'] == line['geometry']
+False
+>>> len(corrected['geometry']) == len(line['geometry'])
+True
+
+```
+
+See ``import mapbox; help(mapbox.MapMatcher)`` for more detailed usage.
diff --git a/mapbox/__init__.py b/mapbox/__init__.py
index e4d15aa..0c25b2b 100644
--- a/mapbox/__init__.py
+++ b/mapbox/__init__.py
@@ -5,6 +5,7 @@ __version__ = "0.5.0"
 from .services.directions import Directions
 from .services.distance import Distance
 from .services.geocoding import Geocoder, InvalidPlaceTypeError
+from .services.mapmatching import MapMatcher
 from .services.surface import Surface
-from .services.uploads import Uploader
 from .services.static import Static
+from .services.uploads import Uploader
diff --git a/mapbox/services/mapmatching.py b/mapbox/services/mapmatching.py
new file mode 100644
index 0000000..3dd5d92
--- /dev/null
+++ b/mapbox/services/mapmatching.py
@@ -0,0 +1,37 @@
+import json
+
+from uritemplate import URITemplate
+
+from mapbox.services.base import Service
+
+
+class MapMatcher(Service):
+
+    def __init__(self, access_token=None):
+        self.baseuri = 'https://api.mapbox.com/matching/v4'
+        self.session = self.get_session(access_token)
+
+    def _validate_profile(self, profile):
+        valid_profiles = ['mapbox.driving', 'mapbox.cycling', 'mapbox.walking']
+        if profile not in valid_profiles:
+            raise ValueError("{} is not a valid profile".format(profile))
+        return profile
+
+    def match(self, feature, profile='mapbox.driving'):
+        profile = self._validate_profile(profile)
+        # todo validate single feature with linestring geometry up to 100 pts
+        geojson_line_feature = json.dumps(feature)
+
+        uri = URITemplate('%s/{profile}.json' % self.baseuri).expand(
+            profile=profile)
+
+        res = self.session.post(uri, data=geojson_line_feature,
+                                headers={'Content-Type': 'application/json'})
+        self.handle_http_error(res)
+
+        def geojson():
+            return res.json()
+
+        res.geojson = geojson
+
+        return res


Test patch:
diff --git a/tests/test_mapmatching.py b/tests/test_mapmatching.py
new file mode 100644
index 0000000..2277866
--- /dev/null
+++ b/tests/test_mapmatching.py
@@ -0,0 +1,47 @@
+import pytest
+import responses
+
+import mapbox
+
+@pytest.fixture
+def line_feature():
+    return {
+        "type": "Feature",
+        "properties": {
+            "coordTimes": [
+                "2015-04-21T06:00:00Z",
+                "2015-04-21T06:00:05Z",
+                "2015-04-21T06:00:10Z",
+                "2015-04-21T06:00:15Z",
+                "2015-04-21T06:00:20Z"]},
+        "geometry": {
+            "type": "LineString",
+            "coordinates": [
+                [13.418946862220764, 52.50055852688439],
+                [13.419011235237122, 52.50113000479732],
+                [13.419756889343262, 52.50171780290061],
+                [13.419885635375975, 52.50237416816131],
+                [13.420631289482117, 52.50294888790448]]}}
+
+
+@responses.activate
+def test_matching(line_feature):
+
+    body = '{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"confidence":0.8165504318718629,"matchedPoints":[[13.418805122375488,52.5005989074707],[13.419145584106445,52.501094818115234],[13.419618606567383,52.50175094604492],[13.420042037963867,52.50233459472656],[13.420494079589844,52.50298309326172]],"indices":[0,1,2,3,4]},"geometry":{"type":"LineString","coordinates":[[13.418805,52.500599],[13.418851,52.500659],[13.419121,52.501057],[13.419146,52.501095],[13.419276,52.501286],[13.419446,52.501518],[13.419619,52.501753],[13.419981,52.502249],[13.420042,52.502335],[13.420494,52.502984]]}}]}'
+
+    responses.add(
+        responses.POST,
+        'https://api.mapbox.com/matching/v4/mapbox.driving.json?access_token=pk.test',
+        match_querystring=True,
+        body=body, status=200,
+        content_type='application/json')
+
+    service = mapbox.MapMatcher(access_token='pk.test')
+    res = service.match(line_feature)
+    assert res.status_code == 200
+
+
+def test_invalid_profile(line_feature):
+    service = mapbox.MapMatcher(access_token='pk.test')
+    with pytest.raises(ValueError):
+        service.match(line_feature, profile="covered_wagon")