[geo] add support for deck.gl's path layer

Works with json and polyline data.

(cherry picked from commit 63e1cf77f99aba5845e593b0a1c4512085d16c64)
diff --git a/setup.py b/setup.py
index 8d0dba3..0ecd1ef 100644
--- a/setup.py
+++ b/setup.py
@@ -66,6 +66,7 @@
         'pandas==0.20.3',
         'parsedatetime==2.0.0',
         'pathlib2==2.3.0',
+        'polyline==1.3.2',
         'pydruid==0.3.1',
         'PyHive>=0.4.0',
         'python-dateutil==2.6.0',
diff --git a/superset/assets/images/viz_thumbnails/deck_path.png b/superset/assets/images/viz_thumbnails/deck_path.png
new file mode 100644
index 0000000..eede9da
--- /dev/null
+++ b/superset/assets/images/viz_thumbnails/deck_path.png
Binary files differ
diff --git a/superset/assets/javascripts/explore/stores/controls.jsx b/superset/assets/javascripts/explore/stores/controls.jsx
index b18039f..dfb45b2 100644
--- a/superset/assets/javascripts/explore/stores/controls.jsx
+++ b/superset/assets/javascripts/explore/stores/controls.jsx
@@ -1723,5 +1723,37 @@
       t('Partitions whose height to parent height proportions are ' +
       'below this value are pruned'),
   },
+
+  line_column: {
+    type: 'SelectControl',
+    label: t('Lines column'),
+    default: null,
+    description: t('The database columns that contains lines information'),
+    mapStateToProps: state => ({
+      choices: (state.datasource) ? state.datasource.all_cols : [],
+    }),
+    validators: [v.nonEmpty],
+  },
+  line_type: {
+    type: 'SelectControl',
+    label: t('Lines encoding'),
+    clearable: false,
+    default: 'json',
+    description: t('The encoding format of the lines'),
+    choices: [
+      ['polyline', 'Polyline'],
+      ['json', 'JSON'],
+    ],
+  },
+
+  line_width: {
+    type: 'TextControl',
+    label: t('Line width'),
+    renderTrigger: true,
+    isInt: true,
+    default: 10,
+    description: t('The width of the lines'),
+  },
+
 };
 export default controls;
diff --git a/superset/assets/javascripts/explore/stores/visTypes.js b/superset/assets/javascripts/explore/stores/visTypes.js
index a243cbf..416e5a6 100644
--- a/superset/assets/javascripts/explore/stores/visTypes.js
+++ b/superset/assets/javascripts/explore/stores/visTypes.js
@@ -397,6 +397,28 @@
     },
   },
 
+  deck_path: {
+    label: t('Deck.gl - Grid'),
+    requiresTime: true,
+    controlPanelSections: [
+      {
+        label: t('Query'),
+        expanded: true,
+        controlSetRows: [
+          ['line_column', 'line_type'],
+        ],
+      },
+      {
+        label: t('Map'),
+        expanded: true,
+        controlSetRows: [
+          ['mapbox_style', 'viewport'],
+          ['color_picker', 'line_width'],
+        ],
+      },
+    ],
+  },
+
   deck_screengrid: {
     label: t('Deck.gl - Screen grid'),
     requiresTime: true,
diff --git a/superset/assets/visualizations/deckgl/path.jsx b/superset/assets/visualizations/deckgl/path.jsx
new file mode 100644
index 0000000..8162f18
--- /dev/null
+++ b/superset/assets/visualizations/deckgl/path.jsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { PathLayer } from 'deck.gl';
+
+import DeckGLContainer from './DeckGLContainer';
+
+function deckPath(slice, payload, setControlValue) {
+  const fd = slice.formData;
+  const c = fd.color_picker;
+  const fixedColor = [c.r, c.g, c.b, 255 * c.a];
+  const data = payload.data.paths.map((path) => {
+    return {
+      path,
+      width: fd.line_width,
+      color: fixedColor,
+    };
+  });;
+
+  const layer = new PathLayer({
+    id: `path-layer-${slice.containerId}`,
+    data,
+    rounded: true,
+    widthScale: 1,
+  });
+  const viewport = {
+    ...fd.viewport,
+    width: slice.width(),
+    height: slice.height(),
+  };
+  ReactDOM.render(
+    <DeckGLContainer
+      mapboxApiAccessToken={payload.data.mapboxApiKey}
+      viewport={viewport}
+      layers={[layer]}
+      mapStyle={fd.mapbox_style}
+      setControlValue={setControlValue}
+    />,
+    document.getElementById(slice.containerId),
+  );
+}
+module.exports = deckPath;
diff --git a/superset/assets/visualizations/main.js b/superset/assets/visualizations/main.js
index 9976614..ac1ac75 100644
--- a/superset/assets/visualizations/main.js
+++ b/superset/assets/visualizations/main.js
@@ -41,5 +41,6 @@
   deck_screengrid: require('./deckgl/screengrid.jsx'),
   deck_grid: require('./deckgl/grid.jsx'),
   deck_hex: require('./deckgl/hex.jsx'),
+  deck_path: require('./deckgl/path.jsx'),
 };
 export default vizMap;
diff --git a/superset/cli.py b/superset/cli.py
index 16500ac..56ead72 100755
--- a/superset/cli.py
+++ b/superset/cli.py
@@ -146,6 +146,9 @@
     print('Loading flights data')
     data.load_flights()
 
+    print('Loading bart lines data')
+    data.load_bart_lines()
+
 
 @manager.option(
     '-d', '--datasource',
diff --git a/superset/data/__init__.py b/superset/data/__init__.py
index 742a32b..b3cb4a8 100644
--- a/superset/data/__init__.py
+++ b/superset/data/__init__.py
@@ -12,8 +12,9 @@
 import textwrap
 
 import pandas as pd
-from sqlalchemy import BigInteger, Date, DateTime, Float, String
+from sqlalchemy import BigInteger, Date, DateTime, Float, String, Text
 import geohash
+import polyline
 
 from superset import app, db, utils
 from superset.connectors.connector_registry import ConnectorRegistry
@@ -1519,3 +1520,33 @@
     db.session.merge(obj)
     db.session.commit()
     obj.fetch_metadata()
+
+
+def load_bart_lines():
+    tbl_name = 'bart_lines'
+    with gzip.open(os.path.join(DATA_FOLDER, 'bart-lines.json.gz')) as f:
+        df = pd.read_json(f, encoding='latin-1')
+        df['path_json'] = df.path.map(json.dumps)
+        df['polyline'] = df.path.map(polyline.encode)
+        del df['path']
+    df.to_sql(
+        tbl_name,
+        db.engine,
+        if_exists='replace',
+        chunksize=500,
+        dtype={
+            'color': String(255),
+            'name': String(255),
+            'polyline': Text,
+            'path_json': Text,
+        },
+        index=False)
+    print("Creating table {} reference".format(tbl_name))
+    tbl = db.session.query(TBL).filter_by(table_name=tbl_name).first()
+    if not tbl:
+        tbl = TBL(table_name=tbl_name)
+    tbl.description = "BART lines"
+    tbl.database = get_or_create_main_db()
+    db.session.merge(tbl)
+    db.session.commit()
+    tbl.fetch_metadata()
diff --git a/superset/data/bart-lines.json.gz b/superset/data/bart-lines.json.gz
new file mode 100644
index 0000000..91f50fb
--- /dev/null
+++ b/superset/data/bart-lines.json.gz
Binary files differ
diff --git a/superset/viz.py b/superset/viz.py
index 6551577..a398666 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -30,6 +30,7 @@
 import simplejson as json
 from six import PY3, string_types, text_type
 from six.moves import reduce
+import polyline
 
 from superset import app, cache, get_manifest_file, utils
 from superset.utils import DTTM_ALIAS, merge_extra_filters
@@ -1814,13 +1815,14 @@
         gb = []
 
         spatial = fd.get('spatial')
-        if spatial.get('type') == 'latlong':
-            gb += [spatial.get('lonCol')]
-            gb += [spatial.get('latCol')]
-        elif spatial.get('type') == 'delimited':
-            gb += [spatial.get('lonlatCol')]
-        elif spatial.get('type') == 'geohash':
-            gb += [spatial.get('geohashCol')]
+        if spatial:
+            if spatial.get('type') == 'latlong':
+                gb += [spatial.get('lonCol')]
+                gb += [spatial.get('latCol')]
+            elif spatial.get('type') == 'delimited':
+                gb += [spatial.get('lonlatCol')]
+            elif spatial.get('type') == 'geohash':
+                gb += [spatial.get('geohashCol')]
 
         if fd.get('dimension'):
             gb += [fd.get('dimension')]
@@ -1881,8 +1883,10 @@
         return super(DeckScatterViz, self).query_obj()
 
     def get_metrics(self):
+        self.metric = None
         if self.point_radius_fixed.get('type') == 'metric':
-            return [self.point_radius_fixed.get('value')]
+            self.metric = self.point_radius_fixed.get('value')
+            return [self.metric]
         return None
 
     def get_properties(self, d):
@@ -1917,6 +1921,36 @@
     verbose_name = _('Deck.gl - 3D Grid')
 
 
+class DeckPathViz(BaseDeckGLViz):
+
+    """deck.gl's PathLayer"""
+
+    viz_type = 'deck_path'
+    verbose_name = _('Deck.gl - Paths')
+    deser_map = {
+        'json': json.loads,
+        'polyline': polyline.decode,
+    }
+
+    def query_obj(self):
+        d = super(DeckPathViz, self).query_obj()
+        d['groupby'] = []
+        d['metrics'] = []
+        d['columns'] = [self.form_data.get('line_column')]
+        return d
+
+    def get_data(self, df):
+        fd = self.form_data
+        deser = self.deser_map[fd.get('line_type')]
+        paths = [deser(s) for s in df[fd.get('line_column')]]
+
+        d = {
+            'mapboxApiKey': config.get('MAPBOX_API_KEY'),
+            'paths': paths,
+        }
+        return d
+
+
 class DeckHex(BaseDeckGLViz):
 
     """deck.gl's DeckLayer"""